Repository: alibaba/sentinel-golang
Pull request #338 (2020-12-03 15:05:05)

Support stat sliding window bucket count config of circuit breaker
<!--  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 stat sliding window bucket count config of circuit breaker.

### Does this pull request fix one issue?
#334 

<!--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/338?src=pr&el=h1) Report
> Merging [#338](https://codecov.io/gh/alibaba/sentinel-golang/pull/338?src=pr&el=desc) (dcfaced) into [master](https://codecov.io/gh/alibaba/sentinel-golang/commit/365844bd4fae2fdcfaaf35ed08932fbc8eb8578e?el=desc) (365844b) will **increase** coverage by `0.12%`.
> The diff coverage is `80.00%`.

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

```diff
@@            Coverage Diff             @@
##           master     #338      +/-   ##
==========================================
+ Coverage   50.75%   50.88%   +0.12%     
==========================================
  Files          69       69              
  Lines        3822     3836      +14     
==========================================
+ Hits         1940     1952      +12     
- Misses       1614     1615       +1     
- Partials      268      269       +1     
```


| [Impacted Files](https://codecov.io/gh/alibaba/sentinel-golang/pull/338?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [core/circuitbreaker/rule\_manager.go](https://codecov.io/gh/alibaba/sentinel-golang/pull/338/diff?src=pr&el=tree#diff-Y29yZS9jaXJjdWl0YnJlYWtlci9ydWxlX21hbmFnZXIuZ28=) | `72.28% <0.00%> (-0.80%)` | :arrow_down: |
| [core/circuitbreaker/rule.go](https://codecov.io/gh/alibaba/sentinel-golang/pull/338/diff?src=pr&el=tree#diff-Y29yZS9jaXJjdWl0YnJlYWtlci9ydWxlLmdv) | `58.97% <83.33%> (+12.30%)` | :arrow_up: |
| [core/circuitbreaker/circuit\_breaker.go](https://codecov.io/gh/alibaba/sentinel-golang/pull/338/diff?src=pr&el=tree#diff-Y29yZS9jaXJjdWl0YnJlYWtlci9jaXJjdWl0X2JyZWFrZXIuZ28=) | `67.55% <100.00%> (+0.32%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/alibaba/sentinel-golang/pull/338?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/338?src=pr&el=footer). Last update [365844b...dcfaced](https://codecov.io/gh/alibaba/sentinel-golang/pull/338?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/api/init.go b/api/init.go
index d72c60d..eac11b7 100644
--- a/api/init.go
+++ b/api/init.go
@@ -19,7 +19,7 @@ import (
 
 	"github.com/alibaba/sentinel-golang/core/config"
 	"github.com/alibaba/sentinel-golang/core/log/metric"
-	"github.com/alibaba/sentinel-golang/core/system"
+	"github.com/alibaba/sentinel-golang/core/system_metric"
 	"github.com/alibaba/sentinel-golang/util"
 	"github.com/pkg/errors"
 )
@@ -85,7 +85,7 @@ func initCoreComponents() error {
 	}
 
 	if config.SystemStatCollectIntervalMs() > 0 {
-		system.InitCollector(config.SystemStatCollectIntervalMs())
+		system_metric.InitCollector(config.SystemStatCollectIntervalMs())
 	}
 
 	if config.UseCacheTime() {
diff --git a/core/circuitbreaker/circuit_breaker.go b/core/circuitbreaker/circuit_breaker.go
index 59e15a4..1775ea0 100644
--- a/core/circuitbreaker/circuit_breaker.go
+++ b/core/circuitbreaker/circuit_breaker.go
@@ -237,8 +237,9 @@ func newSlowRtCircuitBreakerWithStat(r *Rule, stat *slowRequestLeapArray) *slowR
 
 func newSlowRtCircuitBreaker(r *Rule) (*slowRtCircuitBreaker, error) {
 	interval := r.StatIntervalMs
+	bucketCount := getRuleStatSlidingWindowBucketCount(r)
 	stat := &slowRequestLeapArray{}
-	leapArray, err := sbase.NewLeapArray(1, interval, stat)
+	leapArray, err := sbase.NewLeapArray(bucketCount, interval, stat)
 	if err != nil {
 		return nil, err
 	}
@@ -421,8 +422,9 @@ func newErrorRatioCircuitBreakerWithStat(r *Rule, stat *errorCounterLeapArray) *
 
 func newErrorRatioCircuitBreaker(r *Rule) (*errorRatioCircuitBreaker, error) {
 	interval := r.StatIntervalMs
+	bucketCount := getRuleStatSlidingWindowBucketCount(r)
 	stat := &errorCounterLeapArray{}
-	leapArray, err := sbase.NewLeapArray(1, interval, stat)
+	leapArray, err := sbase.NewLeapArray(bucketCount, interval, stat)
 	if err != nil {
 		return nil, err
 	}
@@ -599,8 +601,9 @@ func newErrorCountCircuitBreakerWithStat(r *Rule, stat *errorCounterLeapArray) *
 
 func newErrorCountCircuitBreaker(r *Rule) (*errorCountCircuitBreaker, error) {
 	interval := r.StatIntervalMs
+	bucketCount := getRuleStatSlidingWindowBucketCount(r)
 	stat := &errorCounterLeapArray{}
-	leapArray, err := sbase.NewLeapArray(1, interval, stat)
+	leapArray, err := sbase.NewLeapArray(bucketCount, interval, stat)
 	if err != nil {
 		return nil, err
 	}
diff --git a/core/circuitbreaker/rule.go b/core/circuitbreaker/rule.go
index 355ab64..cea8897 100644
--- a/core/circuitbreaker/rule.go
+++ b/core/circuitbreaker/rule.go
@@ -61,7 +61,14 @@ type Rule struct {
 	// that can trigger circuit breaking.
 	MinRequestAmount uint64 `json:"minRequestAmount"`
 	// StatIntervalMs represents statistic time interval of the internal circuit breaker (in ms).
+	// Currently the statistic interval is collected by sliding window.
 	StatIntervalMs uint32 `json:"statIntervalMs"`
+	// StatSlidingWindowBucketCount represents the bucket count of statistic sliding window.
+	// The statistic will be more precise as the bucket count increases, but the memory cost increases too.
+	// The following must be true — “StatIntervalMs % StatSlidingWindowBucketCount == 0”,
+	// otherwise StatSlidingWindowBucketCount will be replaced by 1.
+	// If it is not set, default value 1 will be used.
+	StatSlidingWindowBucketCount uint32 `json:"statSlidingWindowBucketCount"`
 	// MaxAllowedRtMs indicates that any invocation whose response time exceeds this value (in ms)
 	// will be recorded as a slow request.
 	// MaxAllowedRtMs only takes effect for SlowRequestRatio strategy
@@ -75,15 +82,16 @@ type Rule struct {
 
 func (r *Rule) String() string {
 	// fallback string
-	return fmt.Sprintf("{id=%s,resource=%s, strategy=%s, RetryTimeoutMs=%d, MinRequestAmount=%d, StatIntervalMs=%d, MaxAllowedRtMs=%d, Threshold=%f}",
-		r.Id, r.Resource, r.Strategy, r.RetryTimeoutMs, r.MinRequestAmount, r.StatIntervalMs, r.MaxAllowedRtMs, r.Threshold)
+	return fmt.Sprintf("{id=%s, resource=%s, strategy=%s, RetryTimeoutMs=%d, MinRequestAmount=%d, StatIntervalMs=%d, StatSlidingWindowBucketCount=%d, MaxAllowedRtMs=%d, Threshold=%f}",
+		r.Id, r.Resource, r.Strategy, r.RetryTimeoutMs, r.MinRequestAmount, r.StatIntervalMs, r.StatSlidingWindowBucketCount, r.MaxAllowedRtMs, r.Threshold)
 }
 
 func (r *Rule) isStatReusable(newRule *Rule) bool {
 	if newRule == nil {
 		return false
 	}
-	return r.Resource == newRule.Resource && r.Strategy == newRule.Strategy && r.StatIntervalMs == newRule.StatIntervalMs
+	return r.Resource == newRule.Resource && r.Strategy == newRule.Strategy && r.StatIntervalMs == newRule.StatIntervalMs &&
+		r.StatSlidingWindowBucketCount == newRule.StatSlidingWindowBucketCount
 }
 
 func (r *Rule) ResourceName() string {
@@ -95,7 +103,7 @@ func (r *Rule) isEqualsToBase(newRule *Rule) bool {
 		return false
 	}
 	return r.Resource == newRule.Resource && r.Strategy == newRule.Strategy && r.RetryTimeoutMs == newRule.RetryTimeoutMs &&
-		r.MinRequestAmount == newRule.MinRequestAmount && r.StatIntervalMs == newRule.StatIntervalMs
+		r.MinRequestAmount == newRule.MinRequestAmount && r.StatIntervalMs == newRule.StatIntervalMs && r.StatSlidingWindowBucketCount == newRule.StatSlidingWindowBucketCount
 }
 
 func (r *Rule) isEqualsTo(newRule *Rule) bool {
@@ -114,3 +122,16 @@ func (r *Rule) isEqualsTo(newRule *Rule) bool {
 		return false
 	}
 }
+
+func getRuleStatSlidingWindowBucketCount(r *Rule) uint32 {
+	interval := r.StatIntervalMs
+	bucketCount := r.StatSlidingWindowBucketCount
+	if bucketCount == 0 {
+		bucketCount = 1
+	} else {
+		if interval%bucketCount != 0 {
+			bucketCount = 1
+		}
+	}
+	return bucketCount
+}
diff --git a/core/circuitbreaker/rule_manager.go b/core/circuitbreaker/rule_manager.go
index 8f3821d..fac3147 100644
--- a/core/circuitbreaker/rule_manager.go
+++ b/core/circuitbreaker/rule_manager.go
@@ -393,5 +393,8 @@ func IsValid(r *Rule) error {
 	if r.Strategy == ErrorRatio && r.Threshold > 1.0 {
 		return errors.New("invalid error ratio threshold (valid range: [0.0, 1.0])")
 	}
+	if r.StatSlidingWindowBucketCount != 0 && r.StatIntervalMs%r.StatSlidingWindowBucketCount != 0 {
+		logging.Warn("[CircuitBreaker IsValid] The following must be true: StatIntervalMs % StatSlidingWindowBucketCount == 0. StatSlidingWindowBucketCount will be replaced by 1", "rule", r)
+	}
 	return nil
 }
diff --git a/core/system/slot.go b/core/system/slot.go
index 388ccc9..e4c0888 100644
--- a/core/system/slot.go
+++ b/core/system/slot.go
@@ -17,6 +17,7 @@ package system
 import (
 	"github.com/alibaba/sentinel-golang/core/base"
 	"github.com/alibaba/sentinel-golang/core/stat"
+	"github.com/alibaba/sentinel-golang/core/system_metric"
 )
 
 const (
@@ -87,7 +88,7 @@ func (s *AdaptiveSlot) doCheckRule(rule *Rule) (bool, string, float64) {
 		}
 		return res, msg, rt
 	case Load:
-		l := CurrentLoad()
+		l := system_metric.CurrentLoad()
 		if l > threshold {
 			if rule.Strategy != BBR || !checkBbrSimple() {
 				msg = "system load check blocked"
@@ -96,7 +97,7 @@ func (s *AdaptiveSlot) doCheckRule(rule *Rule) (bool, string, float64) {
 		}
 		return true, "", l
 	case CpuUsage:
-		c := CurrentCpuUsage()
+		c := system_metric.CurrentCpuUsage()
 		if c > threshold {
 			if rule.Strategy != BBR || !checkBbrSimple() {
 				msg = "system cpu usage check blocked"
diff --git a/core/system/sys_stat.go b/core/system_metric/sys_metric_stat.go
similarity index 86%
rename from core/system/sys_stat.go
rename to core/system_metric/sys_metric_stat.go
index e54fa96..0f16427 100644
--- a/core/system/sys_stat.go
+++ b/core/system_metric/sys_metric_stat.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package system
+package system_metric
 
 import (
 	"math"
@@ -27,7 +27,7 @@ import (
 )
 
 const (
-	notRetrievedValue float64 = -1.0
+	NotRetrievedValue float64 = -1.0
 )
 
 var (
@@ -41,8 +41,8 @@ var (
 )
 
 func init() {
-	currentLoad.Store(notRetrievedValue)
-	currentCpuUsage.Store(notRetrievedValue)
+	currentLoad.Store(NotRetrievedValue)
+	currentCpuUsage.Store(NotRetrievedValue)
 }
 
 func InitCollector(intervalMs uint32) {
@@ -124,15 +124,25 @@ func calculateKernelCpuTick(stat *cpu.TimesStat) float64 {
 func CurrentLoad() float64 {
 	r, ok := currentLoad.Load().(float64)
 	if !ok {
-		return notRetrievedValue
+		return NotRetrievedValue
 	}
 	return r
 }
 
+// Note: SetSystemLoad is used for unit test, the user shouldn't call this function.
+func SetSystemLoad(load float64) {
+	currentLoad.Store(load)
+}
+
 func CurrentCpuUsage() float64 {
 	r, ok := currentCpuUsage.Load().(float64)
 	if !ok {
-		return notRetrievedValue
+		return NotRetrievedValue
 	}
 	return r
 }
+
+// Note: SetSystemCpuUsage is used for unit test, the user shouldn't call this function.
+func SetSystemCpuUsage(cpuUsage float64) {
+	currentCpuUsage.Store(cpuUsage)
+}
diff --git a/example/circuitbreaker/error_count/circuit_breaker_error_count_example.go b/example/circuitbreaker/error_count/circuit_breaker_error_count_example.go
index 19905c6..99dda7e 100644
--- a/example/circuitbreaker/error_count/circuit_breaker_error_count_example.go
+++ b/example/circuitbreaker/error_count/circuit_breaker_error_count_example.go
@@ -58,12 +58,13 @@ func main() {
 	_, err = circuitbreaker.LoadRules([]*circuitbreaker.Rule{
 		// Statistic time span=5s, recoveryTimeout=3s, maxErrorCount=50
 		{
-			Resource:         "abc",
-			Strategy:         circuitbreaker.ErrorCount,
-			RetryTimeoutMs:   3000,
-			MinRequestAmount: 10,
-			StatIntervalMs:   5000,
-			Threshold:        50,
+			Resource:                     "abc",
+			Strategy:                     circuitbreaker.ErrorCount,
+			RetryTimeoutMs:               3000,
+			MinRequestAmount:             10,
+			StatIntervalMs:               5000,
+			StatSlidingWindowBucketCount: 10,
+			Threshold:                    50,
 		},
 	})
 	if err != nil {
diff --git a/example/circuitbreaker/error_ratio/circuit_breaker_error_ratio_example.go b/example/circuitbreaker/error_ratio/circuit_breaker_error_ratio_example.go
index 59bc1f5..f001bf6 100644
--- a/example/circuitbreaker/error_ratio/circuit_breaker_error_ratio_example.go
+++ b/example/circuitbreaker/error_ratio/circuit_breaker_error_ratio_example.go
@@ -58,12 +58,13 @@ func main() {
 	_, err = circuitbreaker.LoadRules([]*circuitbreaker.Rule{
 		// Statistic time span=5s, recoveryTimeout=3s, maxErrorRatio=40%
 		{
-			Resource:         "abc",
-			Strategy:         circuitbreaker.ErrorRatio,
-			RetryTimeoutMs:   3000,
-			MinRequestAmount: 10,
-			StatIntervalMs:   5000,
-			Threshold:        0.4,
+			Resource:                     "abc",
+			Strategy:                     circuitbreaker.ErrorRatio,
+			RetryTimeoutMs:               3000,
+			MinRequestAmount:             10,
+			StatIntervalMs:               5000,
+			StatSlidingWindowBucketCount: 10,
+			Threshold:                    0.4,
 		},
 	})
 	if err != nil {
@@ -83,7 +84,7 @@ func main() {
 					sentinel.TraceError(e, errors.New("biz error"))
 				}
 				// g1 passed
-				time.Sleep(time.Duration(rand.Uint64()%80+10) * time.Millisecond)
+				time.Sleep(time.Duration(rand.Uint64()%80+20) * time.Millisecond)
 				e.Exit()
 			}
 		}
@@ -96,7 +97,7 @@ func main() {
 				time.Sleep(time.Duration(rand.Uint64()%20) * time.Millisecond)
 			} else {
 				// g2 passed
-				time.Sleep(time.Duration(rand.Uint64()%80) * time.Millisecond)
+				time.Sleep(time.Duration(rand.Uint64()%80+40) * time.Millisecond)
 				e.Exit()
 			}
 		}
diff --git a/example/circuitbreaker/slow_rt_ratio/circuit_breaker_slow_rt_ratio_example.go b/example/circuitbreaker/slow_rt_ratio/circuit_breaker_slow_rt_ratio_example.go
index cb1b227..52808fe 100644
--- a/example/circuitbreaker/slow_rt_ratio/circuit_breaker_slow_rt_ratio_example.go
+++ b/example/circuitbreaker/slow_rt_ratio/circuit_breaker_slow_rt_ratio_example.go
@@ -58,13 +58,14 @@ func main() {
 	_, err = circuitbreaker.LoadRules([]*circuitbreaker.Rule{
 		// Statistic time span=5s, recoveryTimeout=3s, slowRtUpperBound=50ms, maxSlowRequestRatio=50%
 		{
-			Resource:         "abc",
-			Strategy:         circuitbreaker.SlowRequestRatio,
-			RetryTimeoutMs:   3000,
-			MinRequestAmount: 10,
-			StatIntervalMs:   5000,
-			MaxAllowedRtMs:   50,
-			Threshold:        0.5,
+			Resource:                     "abc",
+			Strategy:                     circuitbreaker.SlowRequestRatio,
+			RetryTimeoutMs:               3000,
+			MinRequestAmount:             10,
+			StatIntervalMs:               5000,
+			StatSlidingWindowBucketCount: 10,
+			MaxAllowedRtMs:               50,
+			Threshold:                    0.5,
 		},
 	})
 	if err != nil {
@@ -97,7 +98,7 @@ func main() {
 				time.Sleep(time.Duration(rand.Uint64()%20) * time.Millisecond)
 			} else {
 				// g2 passed
-				time.Sleep(time.Duration(rand.Uint64()%80) * time.Millisecond)
+				time.Sleep(time.Duration(rand.Uint64()%80+10) * time.Millisecond)
 				e.Exit()
 			}
 		}


Test patch:
diff --git a/core/circuitbreaker/rule_test.go b/core/circuitbreaker/rule_test.go
new file mode 100644
index 0000000..604ed26
--- /dev/null
+++ b/core/circuitbreaker/rule_test.go
@@ -0,0 +1,461 @@
+// Copyright 1999-2020 Alibaba Group Holding Ltd.
+//
+// Licensed 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 circuitbreaker
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestRuleIsStatReusable(t *testing.T) {
+	cases := []struct {
+		rule1          *Rule
+		rule2          *Rule
+		expectedResult bool
+	}{
+		// nil
+		{
+			rule1: &Rule{
+				Resource:                     "abc",
+				Strategy:                     ErrorCount,
+				RetryTimeoutMs:               3000,
+				MinRequestAmount:             10,
+				StatIntervalMs:               10000,
+				StatSlidingWindowBucketCount: 2,
+				Threshold:                    1.0,
+			},
+			rule2:          nil,
+			expectedResult: false,
+		},
+
+		// different Resource
+		{
+			rule1: &Rule{
+				Resource:                     "abc",
+				Strategy:                     ErrorCount,
+				RetryTimeoutMs:               3000,
+				MinRequestAmount:             10,
+				StatIntervalMs:               10000,
+				StatSlidingWindowBucketCount: 2,
+				Threshold:                    1.0,
+			},
+			rule2: &Rule{
+				Resource:                     "def",
+				Strategy:                     ErrorCount,
+				RetryTimeoutMs:               3000,
+				MinRequestAmount:             10,
+				StatIntervalMs:               10000,
+				StatSlidingWindowBucketCount: 2,
+				Threshold:                    1.0,
+			},
+			expectedResult: false,
+		},
+
+		// different Strategy
+		{
+			rule1: &Rule{
+				Resource:                     "abc",
+				Strategy:                     ErrorCount,
+				RetryTimeoutMs:               3000,
+				MinRequestAmount:             10,
+				StatIntervalMs:               10000,
+				StatSlidingWindowBucketCount: 2,
+				Threshold:                    1.0,
+			},
+			rule2: &Rule{
+				Resource:                     "abc",
+				Strategy:                     ErrorRatio,
+				RetryTimeoutMs:               3000,
+				MinRequestAmount:             10,
+				StatIntervalMs:               10000,
+				StatSlidingWindowBucketCount: 2,
+				Threshold:                    0.5,
+			},
+			expectedResult: false,
+		},
+
+		// different StatIntervalMs
+		{
+			rule1: &Rule{
+				Resource:                     "abc",
+				Strategy:                     ErrorCount,
+				RetryTimeoutMs:               3000,
+				MinRequestAmount:             10,
+				StatIntervalMs:               10000,
+				StatSlidingWindowBucketCount: 2,
+				Threshold:                    1.0,
+			},
+			rule2: &Rule{
+				Resource:                     "abc",
+				Strategy:                     ErrorCount,
+				RetryTimeoutMs:               3000,
+				MinRequestAmount:             10,
+				StatIntervalMs:               5000,
+				StatSlidingWindowBucketCount: 2,
+				Threshold:                    1.0,
+			},
+			expectedResult: false,
+		},
+
+		// different StatSlidingWindowBucketCount
+		{
+			rule1: &Rule{
+				Resource:                     "abc",
+				Strategy:                     ErrorCount,
+				RetryTimeoutMs:               3000,
+				MinRequestAmount:             10,
+				StatIntervalMs:               10000,
+				StatSlidingWindowBucketCount: 2,
+				Threshold:                    1.0,
+			},
+			rule2: &Rule{
+				Resource:                     "abc",
+				Strategy:                     ErrorCount,
+				RetryTimeoutMs:               3000,
+				MinRequestAmount:             10,
+				StatIntervalMs:               10000,
+				StatSlidingWindowBucketCount: 5,
+				Threshold:                    1.0,
+			},
+			expectedResult: false,
+		},
+
+		// different RetryTimeoutMs
+		{
+			rule1: &Rule{
+				Resource:                     "abc",
+				Strategy:                     ErrorCount,
+				RetryTimeoutMs:               3000,
+				MinRequestAmount:             10,
+				StatIntervalMs:               10000,
+				StatSlidingWindowBucketCount: 2,
+				Threshold:                    1.0,
+			},
+			rule2: &Rule{
+				Resource:                     "abc",
+				Strategy:                     ErrorCount,
+				RetryTimeoutMs:               5000,
+				MinRequestAmount:             10,
+				StatIntervalMs:               10000,
+				StatSlidingWindowBucketCount: 2,
+				Threshold:                    1.0,
+			},
+			expectedResult: true,
+		},
+
+		// different MinRequestAmount
+		{
+			rule1: &Rule{
+				Resource:                     "abc",
+				Strategy:                     ErrorCount,
+				RetryTimeoutMs:               3000,
+				MinRequestAmount:             10,
+				StatIntervalMs:               10000,
+				StatSlidingWindowBucketCount: 2,
+				Threshold:                    1.0,
+			},
+			rule2: &Rule{
+				Resource:                     "abc",
+				Strategy:                     ErrorCount,
+				RetryTimeoutMs:               3000,
+				MinRequestAmount:             20,
+				StatIntervalMs:               10000,
+				StatSlidingWindowBucketCount: 2,
+				Threshold:                    1.0,
+			},
+			expectedResult: true,
+		},
+
+		// different Threshold
+		{
+			rule1: &Rule{
+				Resource:                     "abc",
+				Strategy:                     ErrorCount,
+				RetryTimeoutMs:               3000,
+				MinRequestAmount:             10,
+				StatIntervalMs:               10000,
+				StatSlidingWindowBucketCount: 2,
+				Threshold:                    1.0,
+			},
+			rule2: &Rule{
+				Resource:                     "abc",
+				Strategy:                     ErrorCount,
+				RetryTimeoutMs:               3000,
+				MinRequestAmount:             20,
+				StatIntervalMs:               10000,
+				StatSlidingWindowBucketCount: 2,
+				Threshold:                    2.0,
+			},
+			expectedResult: true,
+		},
+	}
+
+	for i, c := range cases {
+		result := c.rule1.isStatReusable(c.rule2)
+		assert.Equal(t, c.expectedResult, result, fmt.Sprintf("case %d got unexpected result", i))
+	}
+}
+
+func TestRuleIsEqualsToBase(t *testing.T) {
+	cases := []struct {
+		rule1          *Rule
+		rule2          *Rule
+		expectedResult bool
+	}{
+		// nil
+		{
+			rule1: &Rule{
+				Resource:                     "abc",
+				Strategy:                     ErrorCount,
+				RetryTimeoutMs:               3000,
+				MinRequestAmount:             10,
+				StatIntervalMs:               10000,
+				StatSlidingWindowBucketCount: 2,
+				Threshold:                    1.0,
+			},
+			rule2:          nil,
+			expectedResult: false,
+		},
+
+		// different Resource
+		{
+			rule1: &Rule{
+				Resource:                     "abc",
+				Strategy:                     ErrorCount,
+				RetryTimeoutMs:               3000,
+				MinRequestAmount:             10,
+				StatIntervalMs:               10000,
+				StatSlidingWindowBucketCount: 2,
+				Threshold:                    1.0,
+			},
+			rule2: &Rule{
+				Resource:                     "def",
+				Strategy:                     ErrorCount,
+				RetryTimeoutMs:               3000,
+				MinRequestAmount:             10,
+				StatIntervalMs:               10000,
+				StatSlidingWindowBucketCount: 2,
+				Threshold:                    1.0,
+			},
+			expectedResult: false,
+		},
+
+		// different Strategy
+		{
+			rule1: &Rule{
+				Resource:                     "abc",
+				Strategy:                     ErrorCount,
+				RetryTimeoutMs:               3000,
+				MinRequestAmount:             10,
+				StatIntervalMs:               10000,
+				StatSlidingWindowBucketCount: 2,
+				Threshold:                    1.0,
+			},
+			rule2: &Rule{
+				Resource:                     "abc",
+				Strategy:                     ErrorRatio,
+				RetryTimeoutMs:               3000,
+				MinRequestAmount:             10,
+				StatIntervalMs:               10000,
+				StatSlidingWindowBucketCount: 2,
+				Threshold:                    0.5,
+			},
+			expectedResult: false,
+		},
+
+		// different StatIntervalMs
+		{
+			rule1: &Rule{
+				Resource:                     "abc",
+				Strategy:                     ErrorCount,
+				RetryTimeoutMs:               3000,
+				MinRequestAmount:             10,
+				StatIntervalMs:               10000,
+				StatSlidingWindowBucketCount: 2,
+				Threshold:                    1.0,
+			},
+			rule2: &Rule{
+				Resource:                     "abc",
+				Strategy:                     ErrorCount,
+				RetryTimeoutMs:               3000,
+				MinRequestAmount:             10,
+				StatIntervalMs:               5000,
+				StatSlidingWindowBucketCount: 2,
+				Threshold:                    1.0,
+			},
+			expectedResult: false,
+		},
+
+		// different StatSlidingWindowBucketCount
+		{
+			rule1: &Rule{
+				Resource:                     "abc",
+				Strategy:                     ErrorCount,
+				RetryTimeoutMs:               3000,
+				MinRequestAmount:             10,
+				StatIntervalMs:               10000,
+				StatSlidingWindowBucketCount: 2,
+				Threshold:                    1.0,
+			},
+			rule2: &Rule{
+				Resource:                     "abc",
+				Strategy:                     ErrorCount,
+				RetryTimeoutMs:               3000,
+				MinRequestAmount:             10,
+				StatIntervalMs:               10000,
+				StatSlidingWindowBucketCount: 5,
+				Threshold:                    1.0,
+			},
+			expectedResult: false,
+		},
+
+		// different RetryTimeoutMs
+		{
+			rule1: &Rule{
+				Resource:                     "abc",
+				Strategy:                     ErrorCount,
+				RetryTimeoutMs:               3000,
+				MinRequestAmount:             10,
+				StatIntervalMs:               10000,
+				StatSlidingWindowBucketCount: 2,
+				Threshold:                    1.0,
+			},
+			rule2: &Rule{
+				Resource:                     "abc",
+				Strategy:                     ErrorCount,
+				RetryTimeoutMs:               5000,
+				MinRequestAmount:             10,
+				StatIntervalMs:               10000,
+				StatSlidingWindowBucketCount: 2,
+				Threshold:                    1.0,
+			},
+			expectedResult: false,
+		},
+
+		// different MinRequestAmount
+		{
+			rule1: &Rule{
+				Resource:                     "abc",
+				Strategy:                     ErrorCount,
+				RetryTimeoutMs:               3000,
+				MinRequestAmount:             10,
+				StatIntervalMs:               10000,
+				StatSlidingWindowBucketCount: 2,
+				Threshold:                    1.0,
+			},
+			rule2: &Rule{
+				Resource:                     "abc",
+				Strategy:                     ErrorCount,
+				RetryTimeoutMs:               3000,
+				MinRequestAmount:             20,
+				StatIntervalMs:               10000,
+				StatSlidingWindowBucketCount: 2,
+				Threshold:                    1.0,
+			},
+			expectedResult: false,
+		},
+	}
+
+	for i, c := range cases {
+		result := c.rule1.isEqualsToBase(c.rule2)
+		assert.Equal(t, c.expectedResult, result, fmt.Sprintf("case %d got unexpected result", i))
+	}
+}
+
+func TestGetRuleStatSlidingWindowBucketCount(t *testing.T) {
+	cases := []struct {
+		rule                *Rule
+		expectedBucketCount uint32
+	}{
+		{
+			rule: &Rule{
+				Resource:         "abc",
+				Strategy:         ErrorCount,
+				RetryTimeoutMs:   3000,
+				MinRequestAmount: 10,
+				StatIntervalMs:   10000,
+				Threshold:        1.0,
+			},
+			expectedBucketCount: 1,
+		},
+		{
+			rule: &Rule{
+				Resource:                     "abc",
+				Strategy:                     ErrorCount,
+				RetryTimeoutMs:               3000,
+				MinRequestAmount:             10,
+				StatIntervalMs:               1000,
+				StatSlidingWindowBucketCount: 1,
+				Threshold:                    1.0,
+			},
+			expectedBucketCount: 1,
+		},
+		{
+			rule: &Rule{
+				Resource:                     "abc",
+				Strategy:                     ErrorCount,
+				RetryTimeoutMs:               3000,
+				MinRequestAmount:             10,
+				StatIntervalMs:               1000,
+				StatSlidingWindowBucketCount: 10,
+				Threshold:                    1.0,
+			},
+			expectedBucketCount: 10,
+		},
+		{
+			rule: &Rule{
+				Resource:                     "abc",
+				Strategy:                     ErrorCount,
+				RetryTimeoutMs:               3000,
+				MinRequestAmount:             10,
+				StatIntervalMs:               1000,
+				StatSlidingWindowBucketCount: 30,
+				Threshold:                    1.0,
+			},
+			expectedBucketCount: 1,
+		},
+		{
+			rule: &Rule{
+				Resource:                     "abc",
+				Strategy:                     ErrorCount,
+				RetryTimeoutMs:               3000,
+				MinRequestAmount:             10,
+				StatIntervalMs:               100,
+				StatSlidingWindowBucketCount: 100,
+				Threshold:                    1.0,
+			},
+			expectedBucketCount: 100,
+		},
+		{
+			rule: &Rule{
+				Resource:                     "abc",
+				Strategy:                     ErrorCount,
+				RetryTimeoutMs:               3000,
+				MinRequestAmount:             10,
+				StatIntervalMs:               100,
+				StatSlidingWindowBucketCount: 200,
+				Threshold:                    1.0,
+			},
+			expectedBucketCount: 1,
+		},
+	}
+
+	for i, c := range cases {
+		bucketCount := getRuleStatSlidingWindowBucketCount(c.rule)
+		assert.Equal(t, c.expectedBucketCount, bucketCount, fmt.Sprintf("case %d got unexpected result", i))
+	}
+}
diff --git a/core/system/slot_test.go b/core/system/slot_test.go
index f6508b1..47ed852 100644
--- a/core/system/slot_test.go
+++ b/core/system/slot_test.go
@@ -19,6 +19,7 @@ import (
 
 	"github.com/alibaba/sentinel-golang/core/base"
 	"github.com/alibaba/sentinel-golang/core/stat"
+	"github.com/alibaba/sentinel-golang/core/system_metric"
 	"github.com/alibaba/sentinel-golang/util"
 	"github.com/stretchr/testify/assert"
 )
@@ -80,38 +81,40 @@ func TestDoCheckRuleLoad(t *testing.T) {
 
 	t.Run("TrueLoad", func(t *testing.T) {
 		isOK, _, v := sas.doCheckRule(rule)
-		assert.True(t, util.Float64Equals(notRetrievedValue, v))
+		assert.True(t, util.Float64Equals(system_metric.NotRetrievedValue, v))
 		assert.Equal(t, true, isOK)
 	})
 
 	t.Run("BBRTrueLoad", func(t *testing.T) {
 		rule.Strategy = BBR
-		currentLoad.Store(float64(1.0))
+		system_metric.SetSystemLoad(1.0)
 		isOK, _, v := sas.doCheckRule(rule)
 		assert.Equal(t, true, isOK)
 		assert.True(t, util.Float64Equals(float64(1.0), v))
-		currentLoad.Store(float64(notRetrievedValue))
+		system_metric.SetSystemLoad(system_metric.NotRetrievedValue)
 	})
 }
 
 func TestDoCheckRuleCpuUsage(t *testing.T) {
 	var sas *AdaptiveSlot
-	rule := &Rule{MetricType: CpuUsage,
-		TriggerCount: 0.5}
+	rule := &Rule{
+		MetricType:   CpuUsage,
+		TriggerCount: 0.5,
+	}
 
 	t.Run("TrueCpuUsage", func(t *testing.T) {
 		isOK, _, v := sas.doCheckRule(rule)
-		assert.True(t, util.Float64Equals(notRetrievedValue, v))
+		assert.True(t, util.Float64Equals(system_metric.NotRetrievedValue, v))
 		assert.Equal(t, true, isOK)
 	})
 
 	t.Run("BBRTrueCpuUsage", func(t *testing.T) {
 		rule.Strategy = BBR
-		currentCpuUsage.Store(float64(0.8))
+		system_metric.SetSystemCpuUsage(0.8)
 		isOK, _, v := sas.doCheckRule(rule)
 		assert.Equal(t, true, isOK)
-		assert.True(t, util.Float64Equals(float64(0.8), v))
-		currentCpuUsage.Store(float64(notRetrievedValue))
+		assert.True(t, util.Float64Equals(0.8, v))
+		system_metric.SetSystemCpuUsage(system_metric.NotRetrievedValue)
 	})
 }
 
diff --git a/core/system/sys_stat_test.go b/core/system_metric/sys_metric_stat_test.go
similarity index 85%
rename from core/system/sys_stat_test.go
rename to core/system_metric/sys_metric_stat_test.go
index ead3261..ce0a82a 100644
--- a/core/system/sys_stat_test.go
+++ b/core/system_metric/sys_metric_stat_test.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package system
+package system_metric
 
 import (
 	"testing"
@@ -23,7 +23,7 @@ import (
 )
 
 func Test_recordCpuUsage(t *testing.T) {
-	defer currentCpuUsage.Store(notRetrievedValue)
+	defer currentCpuUsage.Store(NotRetrievedValue)
 
 	var emptyStat *cpu.TimesStat = nil
 	// total: 2260, user+nice: 950, system+irqs=210
@@ -53,7 +53,7 @@ func Test_recordCpuUsage(t *testing.T) {
 	expected := float64(1600+430-950-210) / (4180 - 2260)
 
 	recordCpuUsage(emptyStat, cur)
-	assert.True(t, util.Float64Equals(notRetrievedValue, CurrentCpuUsage()))
+	assert.True(t, util.Float64Equals(NotRetrievedValue, CurrentCpuUsage()))
 
 	recordCpuUsage(prev, prev)
 	assert.True(t, util.Float64Equals(0.0, CurrentCpuUsage()))
@@ -63,10 +63,10 @@ func Test_recordCpuUsage(t *testing.T) {
 }
 
 func TestCurrentLoad(t *testing.T) {
-	defer currentLoad.Store(notRetrievedValue)
+	defer currentLoad.Store(NotRetrievedValue)
 
 	cLoad := CurrentLoad()
-	assert.True(t, util.Float64Equals(notRetrievedValue, cLoad))
+	assert.True(t, util.Float64Equals(NotRetrievedValue, cLoad))
 
 	v := float64(1.0)
 	currentLoad.Store(v)
@@ -75,10 +75,10 @@ func TestCurrentLoad(t *testing.T) {
 }
 
 func TestCurrentCpuUsage(t *testing.T) {
-	defer currentCpuUsage.Store(notRetrievedValue)
+	defer currentCpuUsage.Store(NotRetrievedValue)
 
 	cpuUsage := CurrentCpuUsage()
-	assert.Equal(t, notRetrievedValue, cpuUsage)
+	assert.Equal(t, NotRetrievedValue, cpuUsage)
 
 	v := float64(0.3)
 	currentCpuUsage.Store(v)