Repository: alibaba/sentinel-golang
Pull request #350 (2020-12-13 07:04:49)

Support mock time and ticker
<!--  Thanks for submitting a pull request! Here are some tips for you:
1. Please make sure you have read and understood the contributing guidelines: https://github.com/alibaba/blob/master/CONTRIBUTING.md
2. Please make sure the PR has a corresponding issue.
-->

### Describe what this PR does / why we need it
Support mock time and ticker. We can use mock time and ticker to do unit test, which can decouple from system real time. 

### Does this pull request fix one issue?

<!--If that, add "Fixes #xxxx" below in the next line. For example, Fixes #15. Otherwise, add "NONE" -->

### Describe how you did it


### Describe how to verify it


### Special notes for reviews

codecov-io: # [Codecov](https://codecov.io/gh/alibaba/sentinel-golang/pull/350?src=pr&el=h1) Report
> Merging [#350](https://codecov.io/gh/alibaba/sentinel-golang/pull/350?src=pr&el=desc) (0523901) into [master](https://codecov.io/gh/alibaba/sentinel-golang/commit/f854c9361dec7d8433aaf57c35ea66a8892bee94?el=desc) (f854c93) will **increase** coverage by `0.36%`.
> The diff coverage is `63.85%`.

[![Impacted file tree graph](https://codecov.io/gh/alibaba/sentinel-golang/pull/350/graphs/tree.svg?width=650&height=150&src=pr&token=SrpzbhKA3E)](https://codecov.io/gh/alibaba/sentinel-golang/pull/350?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #350      +/-   ##
==========================================
+ Coverage   50.83%   51.20%   +0.36%     
==========================================
  Files          69       69              
  Lines        3842     3912      +70     
==========================================
+ Hits         1953     2003      +50     
- Misses       1621     1639      +18     
- Partials      268      270       +2     
```


| [Impacted Files](https://codecov.io/gh/alibaba/sentinel-golang/pull/350?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [core/hotspot/slot.go](https://codecov.io/gh/alibaba/sentinel-golang/pull/350/diff?src=pr&el=tree#diff-Y29yZS9ob3RzcG90L3Nsb3QuZ28=) | `27.77% <0.00%> (ø)` | |
| [core/log/metric/aggregator.go](https://codecov.io/gh/alibaba/sentinel-golang/pull/350/diff?src=pr&el=tree#diff-Y29yZS9sb2cvbWV0cmljL2FnZ3JlZ2F0b3IuZ28=) | `25.86% <0.00%> (ø)` | |
| [core/log/metric/writer.go](https://codecov.io/gh/alibaba/sentinel-golang/pull/350/diff?src=pr&el=tree#diff-Y29yZS9sb2cvbWV0cmljL3dyaXRlci5nbw==) | `0.00% <0.00%> (ø)` | |
| [core/system/sys\_stat.go](https://codecov.io/gh/alibaba/sentinel-golang/pull/350/diff?src=pr&el=tree#diff-Y29yZS9zeXN0ZW0vc3lzX3N0YXQuZ28=) | `46.15% <0.00%> (ø)` | |
| [ext/datasource/file/refreshable\_file.go](https://codecov.io/gh/alibaba/sentinel-golang/pull/350/diff?src=pr&el=tree#diff-ZXh0L2RhdGFzb3VyY2UvZmlsZS9yZWZyZXNoYWJsZV9maWxlLmdv) | `49.29% <0.00%> (ø)` | |
| [core/flow/slot.go](https://codecov.io/gh/alibaba/sentinel-golang/pull/350/diff?src=pr&el=tree#diff-Y29yZS9mbG93L3Nsb3QuZ28=) | `48.38% <50.00%> (+1.72%)` | :arrow_up: |
| [util/time.go](https://codecov.io/gh/alibaba/sentinel-golang/pull/350/diff?src=pr&el=tree#diff-dXRpbC90aW1lLmdv) | `71.05% <70.27%> (-0.38%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/alibaba/sentinel-golang/pull/350?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/alibaba/sentinel-golang/pull/350?src=pr&el=footer). Last update [f854c93...0523901](https://codecov.io/gh/alibaba/sentinel-golang/pull/350?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/core/flow/slot.go b/core/flow/slot.go
index db79d2c..4fc66fa 100644
--- a/core/flow/slot.go
+++ b/core/flow/slot.go
@@ -15,11 +15,10 @@
 package flow
 
 import (
-	"time"
-
 	"github.com/alibaba/sentinel-golang/core/base"
 	"github.com/alibaba/sentinel-golang/core/stat"
 	"github.com/alibaba/sentinel-golang/logging"
+	"github.com/alibaba/sentinel-golang/util"
 	"github.com/pkg/errors"
 )
 
@@ -65,7 +64,7 @@ func (s *Slot) Check(ctx *base.EntryContext) *base.TokenResult {
 		if r.Status() == base.ResultStatusShouldWait {
 			if nanosToWait := r.NanosToWait(); nanosToWait > 0 {
 				// Handle waiting action.
-				time.Sleep(nanosToWait)
+				util.Sleep(nanosToWait)
 			}
 			continue
 		}
diff --git a/core/hotspot/slot.go b/core/hotspot/slot.go
index e6ac4c9..26fc4c8 100644
--- a/core/hotspot/slot.go
+++ b/core/hotspot/slot.go
@@ -15,10 +15,9 @@
 package hotspot
 
 import (
-	"time"
-
 	"github.com/alibaba/sentinel-golang/core/base"
 	"github.com/alibaba/sentinel-golang/logging"
+	"github.com/alibaba/sentinel-golang/util"
 )
 
 const (
@@ -88,7 +87,7 @@ func (s *Slot) Check(ctx *base.EntryContext) *base.TokenResult {
 		if r.Status() == base.ResultStatusShouldWait {
 			if nanosToWait := r.NanosToWait(); nanosToWait > 0 {
 				// Handle waiting action.
-				time.Sleep(nanosToWait)
+				util.Sleep(nanosToWait)
 			}
 			continue
 		}
diff --git a/core/log/metric/aggregator.go b/core/log/metric/aggregator.go
index 43a3539..8adc60c 100644
--- a/core/log/metric/aggregator.go
+++ b/core/log/metric/aggregator.go
@@ -58,11 +58,11 @@ func InitTask() (err error) {
 		// Schedule the log flushing task
 		go util.RunWithRecover(writeTaskLoop)
 		// Schedule the log aggregating task
-		ticker := time.NewTicker(time.Duration(flushInterval) * time.Second)
+		ticker := util.NewTicker(time.Duration(flushInterval) * time.Second)
 		go util.RunWithRecover(func() {
 			for {
 				select {
-				case <-ticker.C:
+				case <-ticker.C():
 					doAggregate()
 				case <-stopChan:
 					ticker.Stop()
diff --git a/core/log/metric/writer.go b/core/log/metric/writer.go
index bcaf719..7cddd77 100644
--- a/core/log/metric/writer.go
+++ b/core/log/metric/writer.go
@@ -23,7 +23,6 @@ import (
 	"strconv"
 	"strings"
 	"sync"
-	"time"
 
 	"github.com/alibaba/sentinel-golang/core/base"
 	"github.com/alibaba/sentinel-golang/core/config"
@@ -295,7 +294,7 @@ func NewDefaultMetricLogWriterOfApp(maxSize uint64, maxFileAmount uint32, appNam
 	if maxSize == 0 || maxFileAmount == 0 {
 		return nil, errors.New("invalid maxSize or maxFileAmount")
 	}
-	_, offset := time.Now().Zone()
+	_, offset := util.Now().Zone()
 
 	logDir := config.LogBaseDir()
 	if len(logDir) == 0 {
diff --git a/core/system_metric/sys_metric_stat.go b/core/system_metric/sys_metric_stat.go
index 0f16427..408800c 100644
--- a/core/system_metric/sys_metric_stat.go
+++ b/core/system_metric/sys_metric_stat.go
@@ -53,11 +53,11 @@ func InitCollector(intervalMs uint32) {
 		// Initial retrieval.
 		retrieveAndUpdateSystemStat()
 
-		ticker := time.NewTicker(time.Duration(intervalMs) * time.Millisecond)
+		ticker := util.NewTicker(time.Duration(intervalMs) * time.Millisecond)
 		go util.RunWithRecover(func() {
 			for {
 				select {
-				case <-ticker.C:
+				case <-ticker.C():
 					retrieveAndUpdateSystemStat()
 				case <-ssStopChan:
 					ticker.Stop()
diff --git a/ext/datasource/file/refreshable_file.go b/ext/datasource/file/refreshable_file.go
index f12ccb3..b1e2e05 100644
--- a/ext/datasource/file/refreshable_file.go
+++ b/ext/datasource/file/refreshable_file.go
@@ -107,7 +107,7 @@ func (s *RefreshableFileDataSource) Initialize() error {
 						}
 						retryCount++
 						logging.Error(e, "Failed to add to watcher", "sourceFilePath", s.sourceFilePath)
-						time.Sleep(time.Second)
+						util.Sleep(time.Second)
 					}
 				}
 				if ev.Op&fsnotify.Remove == fsnotify.Remove {
diff --git a/util/time.go b/util/time.go
index de6b534..ef594d2 100644
--- a/util/time.go
+++ b/util/time.go
@@ -15,6 +15,8 @@
 package util
 
 import (
+	"sync"
+	"sync/atomic"
 	"time"
 )
 
@@ -24,6 +26,250 @@ const (
 	UnixTimeUnitOffset = uint64(time.Millisecond / time.Nanosecond)
 )
 
+var (
+	_ Clock = &RealClock{}
+	_ Clock = &MockClock{}
+
+	_ Ticker = &RealTicker{}
+	_ Ticker = &MockTicker{}
+
+	_ TickerCreator = &RealTickerCreator{}
+	_ TickerCreator = &MockTickerCreator{}
+)
+
+var (
+	currentClock         *atomic.Value
+	currentTickerCreator *atomic.Value
+)
+
+func init() {
+	realClock := NewRealClock()
+	currentClock = new(atomic.Value)
+	SetClock(realClock)
+
+	realTickerCreator := NewRealTickerCreator()
+	currentTickerCreator = new(atomic.Value)
+	SetTickerCreator(realTickerCreator)
+}
+
+type Clock interface {
+	Now() time.Time
+	Sleep(d time.Duration)
+	CurrentTimeMillis() uint64
+	CurrentTimeNano() uint64
+}
+
+// clockWrapper is used for atomic operation.
+type clockWrapper struct {
+	clock Clock
+}
+
+// RealClock wraps some APIs of time package.
+type RealClock struct{}
+
+func NewRealClock() *RealClock {
+	return &RealClock{}
+}
+
+func (t *RealClock) Now() time.Time {
+	return time.Now()
+}
+
+func (t *RealClock) Sleep(d time.Duration) {
+	time.Sleep(d)
+}
+
+func (t *RealClock) CurrentTimeMillis() uint64 {
+	tickerNow := CurrentTimeMillsWithTicker()
+	if tickerNow > uint64(0) {
+		return tickerNow
+	}
+	return uint64(time.Now().UnixNano()) / UnixTimeUnitOffset
+}
+
+func (t *RealClock) CurrentTimeNano() uint64 {
+	return uint64(t.Now().UnixNano())
+}
+
+// MockClock is used for testing.
+type MockClock struct {
+	lock sync.RWMutex
+	now  time.Time
+}
+
+func NewMockClock() *MockClock {
+	return &MockClock{
+		now: time.Now(),
+	}
+}
+
+func (t *MockClock) Now() time.Time {
+	t.lock.RLock()
+	defer t.lock.RUnlock()
+	return t.now
+}
+
+func (t *MockClock) Sleep(d time.Duration) {
+	if d <= 0 {
+		return
+	}
+	t.lock.Lock()
+	t.now = t.now.Add(d)
+	t.lock.Unlock()
+	time.Sleep(time.Millisecond)
+}
+
+func (t *MockClock) CurrentTimeMillis() uint64 {
+	return uint64(t.Now().UnixNano()) / UnixTimeUnitOffset
+}
+
+func (t *MockClock) CurrentTimeNano() uint64 {
+	return uint64(t.Now().UnixNano())
+}
+
+// Ticker interface encapsulates operations like time.Ticker.
+type Ticker interface {
+	C() <-chan time.Time
+	Stop()
+}
+
+// RealTicker wraps time.Ticker.
+type RealTicker struct {
+	t *time.Ticker
+}
+
+func NewRealTicker(d time.Duration) *RealTicker {
+	return &RealTicker{
+		t: time.NewTicker(d),
+	}
+}
+
+func (t *RealTicker) C() <-chan time.Time {
+	return t.t.C
+}
+
+func (t *RealTicker) Stop() {
+	t.t.Stop()
+}
+
+// MockTicker is usually used for testing.
+// MockTicker and MockClock are usually used together.
+type MockTicker struct {
+	lock   sync.Mutex
+	period time.Duration
+	c      chan time.Time
+	last   time.Time
+	stop   chan struct{}
+}
+
+func NewMockTicker(d time.Duration) *MockTicker {
+	t := &MockTicker{
+		period: d,
+		c:      make(chan time.Time, 1),
+		last:   Now(),
+		stop:   make(chan struct{}),
+	}
+
+	go t.checkLoop()
+
+	return t
+}
+
+func (t *MockTicker) C() <-chan time.Time {
+	return t.c
+}
+
+func (t *MockTicker) Stop() {
+	close(t.stop)
+}
+
+func (t *MockTicker) check() {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+
+	now := Now()
+	for next := t.last.Add(t.period); !next.After(now); next = next.Add(t.period) {
+		t.last = next
+		select {
+		case <-t.stop:
+			return
+		case t.c <- t.last:
+		default:
+		}
+	}
+}
+
+func (t *MockTicker) checkLoop() {
+	ticker := time.NewTicker(time.Microsecond)
+	for {
+		select {
+		case <-t.stop:
+			return
+		case <-ticker.C:
+		}
+		t.check()
+	}
+}
+
+// TickerCreator is used to create Ticker.
+type TickerCreator interface {
+	NewTicker(d time.Duration) Ticker
+}
+
+// tickerCreatorWrapper is used for atomic operation.
+type tickerCreatorWrapper struct {
+	tickerCreator TickerCreator
+}
+
+// RealTickerCreator is used to creates RealTicker which wraps time.Ticker.
+type RealTickerCreator struct{}
+
+func NewRealTickerCreator() *RealTickerCreator {
+	return &RealTickerCreator{}
+}
+
+func (tc *RealTickerCreator) NewTicker(d time.Duration) Ticker {
+	return NewRealTicker(d)
+}
+
+// MockTickerCreator is used create MockTicker which is usually used for testing.
+// MockTickerCreator and MockClock are usually used together.
+type MockTickerCreator struct{}
+
+func NewMockTickerCreator() *MockTickerCreator {
+	return &MockTickerCreator{}
+}
+
+func (tc *MockTickerCreator) NewTicker(d time.Duration) Ticker {
+	return NewMockTicker(d)
+}
+
+// SetClock sets the clock used by util package.
+// In general, no need to set it. It is usually used for testing.
+func SetClock(c Clock) {
+	currentClock.Store(&clockWrapper{c})
+}
+
+// CurrentClock returns the current clock used by util package.
+func CurrentClock() Clock {
+	return currentClock.Load().(*clockWrapper).clock
+}
+
+// SetClock sets the ticker creator used by util package.
+// In general, no need to set it. It is usually used for testing.
+func SetTickerCreator(tc TickerCreator) {
+	currentTickerCreator.Store(&tickerCreatorWrapper{tc})
+}
+
+// CurrentTickerCreator returns the current ticker creator used by util package.
+func CurrentTickerCreator() TickerCreator {
+	return currentTickerCreator.Load().(*tickerCreatorWrapper).tickerCreator
+}
+
+func NewTicker(d time.Duration) Ticker {
+	return CurrentTickerCreator().NewTicker(d)
+}
+
 // FormatTimeMillis formats Unix timestamp (ms) to time string.
 func FormatTimeMillis(tsMillis uint64) string {
 	return time.Unix(0, int64(tsMillis*UnixTimeUnitOffset)).Format(TimeFormat)
@@ -36,15 +282,21 @@ func FormatDate(tsMillis uint64) string {
 
 // Returns the current Unix timestamp in milliseconds.
 func CurrentTimeMillis() uint64 {
-	// Read from cache first.
-	tickerNow := CurrentTimeMillsWithTicker()
-	if tickerNow > uint64(0) {
-		return tickerNow
-	}
-	return uint64(time.Now().UnixNano()) / UnixTimeUnitOffset
+	return CurrentClock().CurrentTimeMillis()
 }
 
 // Returns the current Unix timestamp in nanoseconds.
 func CurrentTimeNano() uint64 {
-	return uint64(time.Now().UnixNano())
+	return CurrentClock().CurrentTimeNano()
+}
+
+// Now returns the current local time.
+func Now() time.Time {
+	return CurrentClock().Now()
+}
+
+// Sleep pauses the current goroutine for at least the duration d.
+// A negative or zero duration causes Sleep to return immediately.
+func Sleep(d time.Duration) {
+	CurrentClock().Sleep(d)
 }


Test patch:
diff --git a/api/tracer_test.go b/api/tracer_test.go
index ae7ee9e..786522c 100644
--- a/api/tracer_test.go
+++ b/api/tracer_test.go
@@ -20,6 +20,7 @@ import (
 	"time"
 
 	"github.com/alibaba/sentinel-golang/core/base"
+	"github.com/alibaba/sentinel-golang/util"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -28,6 +29,8 @@ var (
 )
 
 func TestTraceErrorToEntry(t *testing.T) {
+	util.SetClock(util.NewMockClock())
+
 	type args struct {
 		entry *base.SentinelEntry
 		err   error
@@ -58,7 +61,7 @@ func TestTraceErrorToEntry(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			TraceError(tt.args.entry, tt.args.err)
-			time.Sleep(time.Millisecond * 10)
+			util.Sleep(time.Millisecond * 10)
 			assert.Equal(t, tests[0].args.entry.Context().Err(), tt.want)
 		})
 	}
diff --git a/core/base/slot_chain_test.go b/core/base/slot_chain_test.go
index ddcebb6..73f1e21 100644
--- a/core/base/slot_chain_test.go
+++ b/core/base/slot_chain_test.go
@@ -20,6 +20,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/alibaba/sentinel-golang/util"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
 )
@@ -301,6 +302,8 @@ func (m *statisticSlotMock) OnCompleted(ctx *EntryContext) {
 }
 
 func TestSlotChain_Entry_Pass_And_Exit(t *testing.T) {
+	util.SetClock(util.NewMockClock())
+
 	sc := NewSlotChain()
 	ctx := sc.GetPooledContext()
 	rw := NewResourceWrapper("abc", ResTypeCommon, Inbound)
@@ -331,7 +334,7 @@ func TestSlotChain_Entry_Pass_And_Exit(t *testing.T) {
 
 	r := sc.Entry(ctx)
 	assert.Equal(t, ResultStatusPass, r.status, "expected to pass but actually blocked")
-	time.Sleep(time.Millisecond * 100)
+	util.Sleep(time.Millisecond * 100)
 
 	sc.exit(ctx)
 
diff --git a/core/flow/tc_throttling_test.go b/core/flow/tc_throttling_test.go
index 985edbb..47ab3f4 100644
--- a/core/flow/tc_throttling_test.go
+++ b/core/flow/tc_throttling_test.go
@@ -21,10 +21,13 @@ import (
 	"time"
 
 	"github.com/alibaba/sentinel-golang/core/base"
+	"github.com/alibaba/sentinel-golang/util"
 	"github.com/stretchr/testify/assert"
 )
 
 func TestThrottlingChecker_DoCheckNoQueueingSingleThread(t *testing.T) {
+	util.SetClock(util.NewMockClock())
+
 	intervalMs := 10000
 	threshold := 50.0
 	timeoutMs := 0
@@ -43,7 +46,7 @@ func TestThrottlingChecker_DoCheckNoQueueingSingleThread(t *testing.T) {
 	for i := 0; i < reqCount; i++ {
 		assert.True(t, tc.DoCheck(nil, 1, threshold).IsBlocked())
 	}
-	time.Sleep(time.Duration(intervalMs/int(threshold)*reqCount+10) * time.Millisecond)
+	util.Sleep(time.Duration(intervalMs/int(threshold)*reqCount+10) * time.Millisecond)
 
 	assert.True(t, tc.DoCheck(nil, 1, threshold) == nil)
 	assert.True(t, tc.DoCheck(nil, 1, threshold).IsBlocked())
diff --git a/core/hotspot/traffic_shaping_test.go b/core/hotspot/traffic_shaping_test.go
index 8ab99fa..9f20c94 100644
--- a/core/hotspot/traffic_shaping_test.go
+++ b/core/hotspot/traffic_shaping_test.go
@@ -111,6 +111,8 @@ func Test_baseTrafficShapingController_performCheckingForConcurrencyMetric(t *te
 }
 
 func Test_defaultTrafficShapingController_performChecking(t *testing.T) {
+	util.SetClock(util.NewMockClock())
+
 	t.Run("Test_defaultTrafficShapingController_performChecking_TimeCounter_Nil", func(t *testing.T) {
 		timeCounter := &counterCacheMock{}
 		tokenCounter := &counterCacheMock{}
@@ -204,7 +206,7 @@ func Test_defaultTrafficShapingController_performChecking(t *testing.T) {
 		timeCounter.On("AddIfAbsent", mock.Anything, mock.Anything).Return(lastAddTokenTime).Times(1)
 
 		tokenCounter.On("AddIfAbsent", mock.Anything, mock.Anything).Return(nil).Times(1)
-		time.Sleep(time.Duration(10) * time.Millisecond)
+		util.Sleep(time.Duration(10) * time.Millisecond)
 		result := c.PerformChecking(arg, 20)
 		assert.True(t, result == nil)
 		assert.True(t, *lastAddTokenTime > currentTimeInMs)
@@ -239,7 +241,7 @@ func Test_defaultTrafficShapingController_performChecking(t *testing.T) {
 		oldQps := new(int64)
 		*oldQps = 50
 		tokenCounter.On("AddIfAbsent", mock.Anything, mock.Anything).Return(oldQps).Times(1)
-		time.Sleep(time.Duration(10) * time.Millisecond)
+		util.Sleep(time.Duration(10) * time.Millisecond)
 		result := c.PerformChecking(arg, 20)
 		assert.True(t, result == nil)
 		assert.True(t, atomic.LoadInt64(lastAddTokenTime) > currentTimeInMs)
diff --git a/core/stat/base/atomic_window_wrap_array_test.go b/core/stat/base/atomic_window_wrap_array_test.go
index 13c2508..2f968a0 100644
--- a/core/stat/base/atomic_window_wrap_array_test.go
+++ b/core/stat/base/atomic_window_wrap_array_test.go
@@ -141,7 +141,7 @@ func Test_atomicBucketWrapArray_compareAndSet(t *testing.T) {
 }
 
 func taskGet(wg *sync.WaitGroup, at *AtomicBucketWrapArray, t *testing.T) {
-	time.Sleep(time.Millisecond * 3)
+	util.Sleep(time.Millisecond * 3)
 	idx := rand.Int() % 20
 	wwPtr := at.get(idx)
 	vInterface := wwPtr.Value.Load()
@@ -159,6 +159,8 @@ func taskGet(wg *sync.WaitGroup, at *AtomicBucketWrapArray, t *testing.T) {
 }
 
 func Test_atomicBucketWrapArray_Concurrency_Get(t *testing.T) {
+	util.SetClock(util.NewMockClock())
+
 	now := uint64(1596199310000)
 	ret := NewAtomicBucketWrapArrayWithTime(int(SampleCount), BucketLengthInMs, now, &leapArrayMock{})
 	for _, ww := range ret.data {
@@ -189,7 +191,7 @@ func Test_atomicBucketWrapArray_Concurrency_Get(t *testing.T) {
 }
 
 func taskSet(wg *sync.WaitGroup, at *AtomicBucketWrapArray, t *testing.T) {
-	time.Sleep(time.Millisecond * 3)
+	util.Sleep(time.Millisecond * 3)
 	idx := rand.Int() % 20
 	ww := at.get(idx)
 	bucket := new(int64)
@@ -207,6 +209,8 @@ func taskSet(wg *sync.WaitGroup, at *AtomicBucketWrapArray, t *testing.T) {
 }
 
 func Test_atomicBucketWrapArray_Concurrency_Set(t *testing.T) {
+	util.SetClock(util.NewMockClock())
+
 	now := uint64(1596199310000)
 	ret := NewAtomicBucketWrapArrayWithTime(int(SampleCount), BucketLengthInMs, now, &leapArrayMock{})
 	for _, ww := range ret.data {
diff --git a/core/stat/base/mutex_test.go b/core/stat/base/mutex_test.go
index c4ede4c..efb9df5 100644
--- a/core/stat/base/mutex_test.go
+++ b/core/stat/base/mutex_test.go
@@ -19,12 +19,14 @@ import (
 	"sync"
 	"testing"
 	"time"
+
+	"github.com/alibaba/sentinel-golang/util"
 )
 
 func Test_Mutex_TryLock(t *testing.T) {
 	var m mutex
 	m.Lock()
-	time.Sleep(time.Second)
+	util.Sleep(time.Second)
 	if m.TryLock() {
 		t.Error("TryLock get lock error")
 	}
diff --git a/core/stat/base/stat_base_benchmark_test.go b/core/stat/base/stat_base_benchmark_test.go
index 5ebe252..2fca276 100644
--- a/core/stat/base/stat_base_benchmark_test.go
+++ b/core/stat/base/stat_base_benchmark_test.go
@@ -17,9 +17,9 @@ package base
 import (
 	"math/rand"
 	"testing"
-	"time"
 
 	"github.com/alibaba/sentinel-golang/core/base"
+	"github.com/alibaba/sentinel-golang/util"
 )
 
 func BenchmarkBucketLeapArray_AddCount_Concurrency1(b *testing.B) {
@@ -75,7 +75,7 @@ func BenchmarkBucketLeapArray_Count_Concurrency1(b *testing.B) {
 	b.ReportAllocs()
 	b.SetParallelism(1)
 	b.ResetTimer()
-	rand.Seed(time.Now().UnixNano())
+	rand.Seed(util.Now().UnixNano())
 	b.RunParallel(func(pb *testing.PB) {
 		for pb.Next() {
 			if rand.Int()%100 >= 80 {
@@ -92,7 +92,7 @@ func BenchmarkBucketLeapArray_Count_Concurrency10(b *testing.B) {
 	b.ReportAllocs()
 	b.SetParallelism(10)
 	b.ResetTimer()
-	rand.Seed(time.Now().UnixNano())
+	rand.Seed(util.Now().UnixNano())
 	b.RunParallel(func(pb *testing.PB) {
 		for pb.Next() {
 			if rand.Int()%100 >= 80 {
@@ -108,7 +108,7 @@ func BenchmarkBucketLeapArray_Count_Concurrency100(b *testing.B) {
 	b.ReportAllocs()
 	b.SetParallelism(100)
 	b.ResetTimer()
-	rand.Seed(time.Now().UnixNano())
+	rand.Seed(util.Now().UnixNano())
 	b.RunParallel(func(pb *testing.PB) {
 		for pb.Next() {
 			if rand.Int()%100 >= 80 {
@@ -125,7 +125,7 @@ func BenchmarkBucketLeapArray_Count_Concurrency1000(b *testing.B) {
 	b.ReportAllocs()
 	b.SetParallelism(1000)
 	b.ResetTimer()
-	rand.Seed(time.Now().UnixNano())
+	rand.Seed(util.Now().UnixNano())
 	b.RunParallel(func(pb *testing.PB) {
 		for pb.Next() {
 			if rand.Int()%100 >= 80 {
diff --git a/ext/datasource/file/refreshable_file_test.go b/ext/datasource/file/refreshable_file_test.go
index e9e6943..0183fc9 100644
--- a/ext/datasource/file/refreshable_file_test.go
+++ b/ext/datasource/file/refreshable_file_test.go
@@ -23,6 +23,7 @@ import (
 	"time"
 
 	"github.com/alibaba/sentinel-golang/ext/datasource"
+	"github.com/alibaba/sentinel-golang/util"
 	"github.com/pkg/errors"
 	"github.com/stretchr/testify/assert"
 	tmock "github.com/stretchr/testify/mock"
@@ -209,9 +210,9 @@ func TestRefreshableFileDataSource_Close(t *testing.T) {
 			t.Errorf("Fail to Initialize datasource, err: %+v", err)
 		}
 
-		time.Sleep(1 * time.Second)
+		util.Sleep(1 * time.Second)
 		s.Close()
-		time.Sleep(1 * time.Second)
+		util.Sleep(1 * time.Second)
 		e := s.watcher.Add(TestSystemRulesFile)
 		assert.True(t, e != nil && strings.Contains(e.Error(), "closed"))
 
@@ -248,12 +249,12 @@ func TestNewFileDataSource_ALL_For_SystemRule(t *testing.T) {
 
 		f.WriteString("\n" + TestSystemRules)
 		f.Sync()
-		time.Sleep(3 * time.Second)
+		util.Sleep(3 * time.Second)
 		mh1.AssertNumberOfCalls(t, "Handle", 2)
 
 		ds.Close()
 		f.Close()
-		time.Sleep(1 * time.Second)
+		util.Sleep(1 * time.Second)
 		e := ds.watcher.Add(TestSystemRulesFile)
 		assert.True(t, e != nil && strings.Contains(e.Error(), "closed"))
 
@@ -285,11 +286,11 @@ func TestNewFileDataSource_ALL_For_SystemRule(t *testing.T) {
 			t.Errorf("Fail to delete test file, err: %+v", err)
 		}
 
-		time.Sleep(3 * time.Second)
+		util.Sleep(3 * time.Second)
 		mh1.AssertNumberOfCalls(t, "Handle", 2)
 
 		ds.Close()
-		time.Sleep(1 * time.Second)
+		util.Sleep(1 * time.Second)
 		e := ds.watcher.Add(TestSystemRulesFile)
 		assert.True(t, e != nil)
 	})
diff --git a/tests/core/circuitbreaker/circuitbreaker_slot_integration_test.go b/tests/core/circuitbreaker/circuitbreaker_slot_integration_test.go
index 0943580..541c78c 100644
--- a/tests/core/circuitbreaker/circuitbreaker_slot_integration_test.go
+++ b/tests/core/circuitbreaker/circuitbreaker_slot_integration_test.go
@@ -28,6 +28,7 @@ import (
 	"github.com/alibaba/sentinel-golang/core/circuitbreaker"
 	"github.com/alibaba/sentinel-golang/core/config"
 	"github.com/alibaba/sentinel-golang/logging"
+	"github.com/alibaba/sentinel-golang/util"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
 )
@@ -60,6 +61,8 @@ func (s *StateChangeListenerMock) OnTransformToHalfOpen(prev circuitbreaker.Stat
 //                 when request exit, rollback the state of cb1 to open
 // Third request: same with second request.
 func TestCircuitBreakerSlotIntegration_Normal(t *testing.T) {
+	util.SetClock(util.NewMockClock())
+
 	circuitbreaker.ClearStateChangeListeners()
 	if clearErr := circuitbreaker.ClearRules(); clearErr != nil {
 		t.Fatal(clearErr)
@@ -109,14 +112,14 @@ func TestCircuitBreakerSlotIntegration_Normal(t *testing.T) {
 	e, b := sentinel.Entry("abc", sentinel.WithSlotChain(sc))
 	assert.True(t, b == nil)
 	sentinel.TraceError(e, errors.New("biz error"))
-	time.Sleep(time.Duration(50) * time.Millisecond)
+	util.Sleep(time.Duration(50) * time.Millisecond)
 	e.Exit()
 	stateListener.AssertNumberOfCalls(t, "OnTransformToOpen", 2)
 	stateListener.AssertNotCalled(t, "OnTransformToClosed")
 	stateListener.AssertNotCalled(t, "OnTransformToHalfOpen")
 
 	// wait circuit breaker1 retry timeout
-	time.Sleep(time.Duration(100) * time.Millisecond)
+	util.Sleep(time.Duration(100) * time.Millisecond)
 
 	// Second circuit breaker1 probes and circuit breaker2 block the request
 	circuitbreaker.ClearStateChangeListeners()
@@ -131,7 +134,7 @@ func TestCircuitBreakerSlotIntegration_Normal(t *testing.T) {
 	stateListener2.AssertCalled(t, "OnTransformToHalfOpen", circuitbreaker.Open, mock.Anything)
 	stateListener2.AssertNumberOfCalls(t, "OnTransformToOpen", 1)
 	stateListener2.AssertCalled(t, "OnTransformToOpen", circuitbreaker.HalfOpen, mock.Anything, mock.Anything)
-	time.Sleep(time.Duration(100) * time.Millisecond)
+	util.Sleep(time.Duration(100) * time.Millisecond)
 
 	// Third, same with second request.
 	circuitbreaker.ClearStateChangeListeners()
@@ -154,6 +157,8 @@ func TestCircuitBreakerSlotIntegration_Normal(t *testing.T) {
 }
 
 func TestCircuitBreakerSlotIntegration_Probe_Succeed(t *testing.T) {
+	util.SetClock(util.NewMockClock())
+
 	circuitbreaker.ClearStateChangeListeners()
 	if clearErr := circuitbreaker.ClearRules(); clearErr != nil {
 		t.Fatal(clearErr)
@@ -194,14 +199,14 @@ func TestCircuitBreakerSlotIntegration_Probe_Succeed(t *testing.T) {
 	// First trigger the circuit breaker
 	e, b := sentinel.Entry("abc", sentinel.WithSlotChain(sc))
 	assert.True(t, b == nil)
-	time.Sleep(time.Duration(50) * time.Millisecond)
+	util.Sleep(time.Duration(50) * time.Millisecond)
 	e.Exit()
 	stateListener.AssertNumberOfCalls(t, "OnTransformToOpen", 1)
 	stateListener.AssertNotCalled(t, "OnTransformToClosed")
 	stateListener.AssertNotCalled(t, "OnTransformToHalfOpen")
 
 	// wait circuit breaker1 retry timeout
-	time.Sleep(time.Duration(100) * time.Millisecond)
+	util.Sleep(time.Duration(100) * time.Millisecond)
 
 	// Second circuit breaker1 probes succeed
 	circuitbreaker.ClearStateChangeListeners()
@@ -225,6 +230,8 @@ func TestCircuitBreakerSlotIntegration_Probe_Succeed(t *testing.T) {
 }
 
 func TestCircuitBreakerSlotIntegration_Concurrency(t *testing.T) {
+	util.SetClock(util.NewMockClock())
+
 	logging.ResetGlobalLoggerLevel(logging.InfoLevel)
 	circuitbreaker.ClearStateChangeListeners()
 	if clearErr := circuitbreaker.ClearRules(); clearErr != nil {
@@ -277,13 +284,13 @@ func TestCircuitBreakerSlotIntegration_Concurrency(t *testing.T) {
 	e, b := sentinel.Entry("abc", sentinel.WithSlotChain(sc))
 	assert.True(t, b == nil)
 	sentinel.TraceError(e, errors.New("biz error"))
-	time.Sleep(time.Duration(50) * time.Millisecond)
+	util.Sleep(time.Duration(50) * time.Millisecond)
 	e.Exit()
 	stateListener.AssertNumberOfCalls(t, "OnTransformToOpen", 2)
 	stateListener.AssertNotCalled(t, "OnTransformToClosed")
 	stateListener.AssertNotCalled(t, "OnTransformToHalfOpen")
 	// wait circuit breaker1 retry timeout
-	time.Sleep(time.Duration(100) * time.Millisecond)
+	util.Sleep(time.Duration(100) * time.Millisecond)
 
 	circuitbreaker.ClearStateChangeListeners()
 	stateListener2 := &StateChangeListenerMock{}
diff --git a/tests/core/misc/flow_slot_load_rule_test.go b/tests/core/misc/flow_slot_load_rule_test.go
index 971c0c6..cb98f77 100644
--- a/tests/core/misc/flow_slot_load_rule_test.go
+++ b/tests/core/misc/flow_slot_load_rule_test.go
@@ -25,6 +25,7 @@ import (
 	"github.com/alibaba/sentinel-golang/core/config"
 	"github.com/alibaba/sentinel-golang/core/flow"
 	"github.com/alibaba/sentinel-golang/logging"
+	"github.com/alibaba/sentinel-golang/util"
 )
 
 func Test_Flow_Slot_LoadRules(t *testing.T) {
@@ -52,7 +53,7 @@ func Test_Flow_Slot_LoadRules(t *testing.T) {
 	}
 
 	go func() {
-		time.Sleep(time.Second * 2)
+		util.Sleep(time.Second * 2)
 		_, err = circuitbreaker.LoadRules([]*circuitbreaker.Rule{
 			// Statistic time span=10s, recoveryTimeout=3s, slowRtUpperBound=50ms, maxSlowRequestRatio=50%
 			{
@@ -70,8 +71,6 @@ func Test_Flow_Slot_LoadRules(t *testing.T) {
 		}
 	}()
 
-	ch := time.NewTicker(time.Second * 3).C
-
 	for i := 0; i < 10; i++ {
 		go func() {
 			for {
@@ -86,5 +85,6 @@ func Test_Flow_Slot_LoadRules(t *testing.T) {
 			}
 		}()
 	}
-	<-ch
+
+	util.Sleep(time.Second * 3)
 }
diff --git a/util/time_test.go b/util/time_test.go
index 85ae52a..0f71215 100644
--- a/util/time_test.go
+++ b/util/time_test.go
@@ -18,6 +18,8 @@ import (
 	"fmt"
 	"testing"
 	"time"
+
+	"github.com/stretchr/testify/assert"
 )
 
 //Time zone should be considered for time related operations
@@ -106,12 +108,98 @@ func TestCurrentTimeNano(t *testing.T) {
 	fmt.Println(got)
 }
 
+func TestMockClock(t *testing.T) {
+	clock := NewMockClock()
+
+	now := clock.Now()
+	assert.Equal(t, clock.CurrentTimeNano(), uint64(now.UnixNano()))
+	assert.Equal(t, clock.CurrentTimeMillis(), uint64(now.UnixNano()/1e6))
+
+	last := now
+	d := 23*time.Hour + 59*time.Minute + 59*time.Second + 999*time.Millisecond + 999*time.Microsecond
+	clock.Sleep(d)
+	assert.Equal(t, last.Add(d), clock.Now())
+}
+
+func TestMockTicker(t *testing.T) {
+	SetClock(NewMockClock())
+	defer SetClock(NewRealClock())
+
+	d := 23*time.Hour + 59*time.Minute + 59*time.Second + 999*time.Millisecond + 999*time.Microsecond
+
+	ticker := NewMockTicker(d)
+	defer ticker.Stop()
+
+	ticked := 0
+
+	ticker.check()
+	select {
+	case <-ticker.C():
+		ticked++
+	default:
+	}
+	assert.Equal(t, ticked, 0)
+
+	Sleep(d - 1)
+	ticker.check()
+	select {
+	case <-ticker.C():
+		ticked++
+	default:
+	}
+	assert.Equal(t, ticked, 0)
+
+	Sleep(1)
+	ticker.check()
+	select {
+	case <-ticker.C():
+		ticked++
+	default:
+	}
+	assert.Equal(t, ticked, 1)
+
+	Sleep(d + 1)
+	ticker.check()
+	select {
+	case <-ticker.C():
+		ticked++
+	default:
+	}
+	assert.Equal(t, ticked, 2)
+
+	Sleep(d - 1)
+	ticker.check()
+	select {
+	case <-ticker.C():
+		ticked++
+	default:
+	}
+	assert.Equal(t, ticked, 3)
+}
+
 func BenchmarkCurrentTimeInMs(b *testing.B) {
+	StartTimeTicker()
+	currentTimeMillisDirect := func() uint64 {
+		tickerNow := CurrentTimeMillsWithTicker()
+		if tickerNow > uint64(0) {
+			return tickerNow
+		}
+		return uint64(time.Now().UnixNano()) / UnixTimeUnitOffset
+	}
+
 	b.ReportAllocs()
 	b.ResetTimer()
-	for i := 0; i < b.N; i++ {
-		CurrentTimeMillis()
-	}
+
+	b.Run("CurrentTimeMillis", func(b *testing.B) {
+		for i := 0; i < b.N; i++ {
+			CurrentTimeMillis()
+		}
+	})
+	b.Run("CurrentTimeMillisDirect", func(b *testing.B) {
+		for i := 0; i < b.N; i++ {
+			currentTimeMillisDirect()
+		}
+	})
 }
 
 func BenchmarkCurrentTimeInMsWithTicker(b *testing.B) {
@@ -121,3 +209,20 @@ func BenchmarkCurrentTimeInMsWithTicker(b *testing.B) {
 		CurrentTimeMillsWithTicker()
 	}
 }
+
+func BenchmarkNow(b *testing.B) {
+	b.Run("util.Now", func(b *testing.B) {
+		b.ReportAllocs()
+		b.ResetTimer()
+		for i := 0; i < b.N; i++ {
+			Now()
+		}
+	})
+	b.Run("time.Now", func(b *testing.B) {
+		b.ReportAllocs()
+		b.ResetTimer()
+		for i := 0; i < b.N; i++ {
+			time.Now()
+		}
+	})
+}