Repository: alibaba/sentinel-golang
Pull request #595 (2025-06-20 08:25:18)

[feature] support regex in flow control and isolation for resource name
<!--  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

By allowing resource names to be defined using regex patterns, users can apply unified rules to groups of resources that match specific naming conventions or dynamic patterns (e.g., RESTful API endpoints with variable path parameters). This enhancement reduces the need for repetitive rule definitions and improves adaptability to evolving service structures

### 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

1. Added a Regex boolean flag to FlowRule and IsolationRule to mark whether the Resource uses a regular expression.
2. Rules with regex patterns are stored in a dedicated regexRulesMap to separate them from static resource rules.
3. Find regex-based rules using pattern matching when getRulesOfResource.
4. Improved performance by caching compiled regex patterns to prevent redundant compilations during repeated evaluations.

### Describe how to verify it

1. Define a flow or isolation rule with Resource set to a regex pattern (e.g., abc/*) and Regex enabled.
5. Trigger requests with resource names like abc/123, abc/456.
6. Validate that the rule is correctly applied based on the configured threshold and strategy.

### Special notes for reviews


Patch:
diff --git a/core/flow/rule.go b/core/flow/rule.go
index efe5019..6803d23 100644
--- a/core/flow/rule.go
+++ b/core/flow/rule.go
@@ -117,6 +117,8 @@ type Rule struct {
 	HighMemUsageThreshold int64 `json:"highMemUsageThreshold"`
 	MemLowWaterMarkBytes  int64 `json:"memLowWaterMarkBytes"`
 	MemHighWaterMarkBytes int64 `json:"memHighWaterMarkBytes"`
+	// Regex indicates whether the rule is a regex rule
+	Regex bool `json:"regex"`
 }
 
 func (r *Rule) isEqualsTo(newRule *Rule) bool {
diff --git a/core/flow/rule_manager.go b/core/flow/rule_manager.go
index 39f3a00..0e23d5e 100644
--- a/core/flow/rule_manager.go
+++ b/core/flow/rule_manager.go
@@ -17,6 +17,7 @@ package flow
 import (
 	"fmt"
 	"reflect"
+	"regexp"
 	"sync"
 
 	"github.com/alibaba/sentinel-golang/core/base"
@@ -41,10 +42,12 @@ type trafficControllerGenKey struct {
 type TrafficControllerMap map[string][]*TrafficShapingController
 
 var (
-	tcGenFuncMap = make(map[trafficControllerGenKey]TrafficControllerGenFunc, 6)
-	tcMap        = make(TrafficControllerMap)
-	tcMux        = new(sync.RWMutex)
-	nopStat      = &standaloneStatistic{
+	tcGenFuncMap    = make(map[trafficControllerGenKey]TrafficControllerGenFunc, 6)
+	tcMap           = make(TrafficControllerMap)
+	tcRegexMap      = make(TrafficControllerMap)
+	tcRegexCacheMap = make(TrafficControllerMap)
+	tcMux           = new(sync.RWMutex)
+	nopStat         = &standaloneStatistic{
 		reuseResourceStat: false,
 		readOnlyMetric:    base.NopReadStat(),
 		writeOnlyMetric:   base.NopWriteStat(),
@@ -204,24 +207,35 @@ func onRuleUpdate(rawResRulesMap map[string][]*Rule) (err error) {
 	start := util.CurrentTimeNano()
 
 	tcMux.RLock()
-	tcMapClone := make(TrafficControllerMap, len(validResRulesMap))
+	tcMapClone := make(TrafficControllerMap, len(tcMap)+len(tcRegexMap))
 	for res, tcs := range tcMap {
 		resTcClone := make([]*TrafficShapingController, 0, len(tcs))
 		resTcClone = append(resTcClone, tcs...)
 		tcMapClone[res] = resTcClone
 	}
+	for res, tcs := range tcRegexMap {
+		resTcClone := make([]*TrafficShapingController, 0, len(tcs))
+		resTcClone = append(resTcClone, tcs...)
+		tcMapClone[res] = resTcClone
+	}
 	tcMux.RUnlock()
 
-	m := make(TrafficControllerMap, len(validResRulesMap))
+	m := make(TrafficControllerMap)
+	regexM := make(TrafficControllerMap)
 	for res, rulesOfRes := range validResRulesMap {
-		newTcsOfRes := buildResourceTrafficShapingController(res, rulesOfRes, tcMapClone[res])
+		newTcsOfRes, newRegexTcsOfRes := buildResourceTrafficShapingController(res, rulesOfRes, tcMapClone[res])
 		if len(newTcsOfRes) > 0 {
 			m[res] = newTcsOfRes
 		}
+		if len(newRegexTcsOfRes) > 0 {
+			regexM[res] = newRegexTcsOfRes
+		}
 	}
 
 	tcMux.Lock()
 	tcMap = m
+	tcRegexMap = regexM
+	tcRegexCacheMap = make(TrafficControllerMap)
 	tcMux.Unlock()
 	currentRules = rawResRulesMap
 
@@ -277,8 +291,9 @@ func onResourceRuleUpdate(res string, rawResRules []*Rule) (err error) {
 	oldResTcs := make([]*TrafficShapingController, 0)
 	tcMux.RLock()
 	oldResTcs = append(oldResTcs, tcMap[res]...)
+	oldResTcs = append(oldResTcs, tcRegexMap[res]...)
 	tcMux.RUnlock()
-	newResTcs := buildResourceTrafficShapingController(res, validResRules, oldResTcs)
+	newResTcs, newRegexResTcs := buildResourceTrafficShapingController(res, validResRules, oldResTcs)
 
 	tcMux.Lock()
 	if len(newResTcs) == 0 {
@@ -286,6 +301,12 @@ func onResourceRuleUpdate(res string, rawResRules []*Rule) (err error) {
 	} else {
 		tcMap[res] = newResTcs
 	}
+	if len(newRegexResTcs) == 0 {
+		delete(tcRegexMap, res)
+	} else {
+		tcRegexMap[res] = newRegexResTcs
+	}
+	tcRegexCacheMap = make(TrafficControllerMap)
 	tcMux.Unlock()
 	currentRules[res] = rawResRules
 	logging.Debug("[Flow onResourceRuleUpdate] Time statistic(ns) for updating flow rule", "timeCost", util.CurrentTimeNano()-start)
@@ -308,6 +329,8 @@ func LoadRulesOfResource(res string, rules []*Rule) (bool, error) {
 		// clear tcMap
 		tcMux.Lock()
 		delete(tcMap, res)
+		delete(tcRegexMap, res)
+		tcRegexCacheMap = make(TrafficControllerMap)
 		tcMux.Unlock()
 		logging.Info("[Flow] clear resource level rules", "resource", res)
 		return true, nil
@@ -329,19 +352,31 @@ func getRules() []*Rule {
 	tcMux.RLock()
 	defer tcMux.RUnlock()
 
-	return rulesFrom(tcMap)
+	return append(rulesFrom(tcMap), rulesFrom(tcRegexMap)...)
 }
 
 // getRulesOfResource returns specific resource's rules。Any changes of rules take effect for flow module
 // getRulesOfResource is an internal interface.
 func getRulesOfResource(res string) []*Rule {
 	tcMux.RLock()
-	defer tcMux.RUnlock()
 
-	resTcs, exist := tcMap[res]
-	if !exist {
-		return nil
+	resTcs := make([]*TrafficShapingController, 0)
+	if tcs, ok := tcMap[res]; ok {
+		resTcs = append(resTcs, tcs...)
+	}
+	cacheRegexTcs, ok := tcRegexCacheMap[res]
+	tcMux.RUnlock()
+
+	if ok {
+		resTcs = append(resTcs, cacheRegexTcs...)
+	} else {
+		tcMux.Lock()
+		regexTcs := regexTcMatches(res)
+		resTcs = append(resTcs, regexTcs...)
+		tcRegexCacheMap[res] = regexTcs
+		tcMux.Unlock()
 	}
+
 	ret := make([]*Rule, 0, len(resTcs))
 	for _, tc := range resTcs {
 		ret = append(ret, tc.BoundRule())
@@ -507,9 +542,28 @@ func RemoveTrafficShapingGenerator(tokenCalculateStrategy TokenCalculateStrategy
 
 func getTrafficControllerListFor(name string) []*TrafficShapingController {
 	tcMux.RLock()
-	defer tcMux.RUnlock()
+	rules := make([]*TrafficShapingController, 0)
+	rules = append(rules, tcMap[name]...)
+	regexCacheRules, ok := tcRegexCacheMap[name]
+	tcMux.RUnlock()
+
+	if ok {
+		rules = append(rules, regexCacheRules...)
+		return rules
+	}
 
-	return tcMap[name]
+	tcMux.Lock()
+	defer tcMux.Unlock()
+	// double check
+	if regexCacheRules, ok = tcRegexCacheMap[name]; ok {
+		rules = append(rules, regexCacheRules...)
+		return rules
+	}
+	regexRules := regexTcMatches(name)
+	tcRegexCacheMap[name] = regexRules
+	rules = append(rules, regexRules...)
+
+	return rules
 }
 
 func calculateReuseIndexFor(r *Rule, oldResTcs []*TrafficShapingController) (equalIdx, reuseStatIdx int) {
@@ -539,8 +593,10 @@ func calculateReuseIndexFor(r *Rule, oldResTcs []*TrafficShapingController) (equ
 }
 
 // buildResourceTrafficShapingController builds TrafficShapingController slice from rules. the resource of rules must be equals to res
-func buildResourceTrafficShapingController(res string, rulesOfRes []*Rule, oldResTcs []*TrafficShapingController) []*TrafficShapingController {
+func buildResourceTrafficShapingController(res string, rulesOfRes []*Rule,
+	oldResTcs []*TrafficShapingController) (simpleControllers []*TrafficShapingController, regexControllers []*TrafficShapingController) {
 	newTcsOfRes := make([]*TrafficShapingController, 0, len(rulesOfRes))
+	newRegexTcsOfRes := make([]*TrafficShapingController, 0, len(rulesOfRes))
 	for _, rule := range rulesOfRes {
 		if res != rule.Resource {
 			logging.Error(errors.Errorf("unmatched resource name expect: %s, actual: %s", res, rule.Resource), "Unmatched resource name in flow.buildResourceTrafficShapingController()", "rule", rule)
@@ -582,9 +638,15 @@ func buildResourceTrafficShapingController(res string, rulesOfRes []*Rule, oldRe
 			// remove old tc from oldResTcs
 			oldResTcs = append(oldResTcs[:reuseStatIdx], oldResTcs[reuseStatIdx+1:]...)
 		}
-		newTcsOfRes = append(newTcsOfRes, tc)
+
+		if tc.rule != nil && tc.rule.Regex {
+			newRegexTcsOfRes = append(newRegexTcsOfRes, tc)
+		} else {
+			newTcsOfRes = append(newTcsOfRes, tc)
+		}
+
 	}
-	return newTcsOfRes
+	return newTcsOfRes, newRegexTcsOfRes
 }
 
 // IsValidRule checks whether the given Rule is valid.
@@ -649,3 +711,35 @@ func IsValidRule(rule *Rule) error {
 
 	return nil
 }
+
+func regexTcMatches(resource string) []*TrafficShapingController {
+	result := make([]*TrafficShapingController, 0)
+	for pattern, controllers := range tcRegexMap {
+		re := regexp.MustCompile(pattern)
+		if !re.MatchString(resource) {
+			continue
+		}
+		controllersCopy := make([]*TrafficShapingController, len(controllers))
+		for resourceName, controller := range controllers {
+			generator, supported := tcGenFuncMap[trafficControllerGenKey{
+				tokenCalculateStrategy: controller.rule.TokenCalculateStrategy,
+				controlBehavior:        controller.rule.ControlBehavior,
+			}]
+			if !supported || generator == nil {
+				logging.Error(errors.New("get trafficShapingController copy failed, unsupported flow control strategy"),
+					"Ignoring the rule due to unsupported control behavior in flow."+
+						"buildResourceTrafficShapingController()", "rule", controller.rule)
+				continue
+			}
+			if tc, e := generator(controller.rule, nil); e == nil && tc != nil {
+				controllersCopy[resourceName] = tc
+			} else {
+				logging.Error(errors.New("get trafficShapingController copy failed, bad generated traffic controller"),
+					"Ignoring the rule due to bad generated traffic controller in flow."+
+						"buildResourceTrafficShapingController()", "rule", controller.rule)
+			}
+		}
+		result = append(result, controllersCopy...)
+	}
+	return result
+}
diff --git a/core/isolation/rule.go b/core/isolation/rule.go
index 1ebe1aa..a608a3d 100644
--- a/core/isolation/rule.go
+++ b/core/isolation/rule.go
@@ -46,13 +46,16 @@ type Rule struct {
 	// Currently Concurrency is supported for concurrency limiting.
 	MetricType MetricType `json:"metricType"`
 	Threshold  uint32     `json:"threshold"`
+	// Regex indicates whether the rule is a regex rule
+	Regex bool `json:"regex"`
 }
 
 func (r *Rule) String() string {
 	b, err := json.Marshal(r)
 	if err != nil {
 		// Return the fallback string
-		return fmt.Sprintf("{Id=%s, Resource=%s, MetricType=%s, Threshold=%d}", r.ID, r.Resource, r.MetricType.String(), r.Threshold)
+		return fmt.Sprintf("{Id=%s, Resource=%s, MetricType=%s, Threshold=%d, Regex=%v}", r.ID, r.Resource,
+			r.MetricType.String(), r.Threshold, r.Regex)
 	}
 	return string(b)
 }
diff --git a/core/isolation/rule_manager.go b/core/isolation/rule_manager.go
index 669ae61..353977f 100644
--- a/core/isolation/rule_manager.go
+++ b/core/isolation/rule_manager.go
@@ -16,6 +16,7 @@ package isolation
 
 import (
 	"reflect"
+	"regexp"
 	"sync"
 
 	"github.com/alibaba/sentinel-golang/logging"
@@ -24,10 +25,12 @@ import (
 )
 
 var (
-	ruleMap       = make(map[string][]*Rule)
-	rwMux         = &sync.RWMutex{}
-	currentRules  = make(map[string][]*Rule, 0)
-	updateRuleMux = new(sync.Mutex)
+	ruleMap           = make(map[string][]*Rule)
+	regexRuleMap      = make(map[string][]*Rule)
+	regexCacheRuleMap = make(map[string][]*Rule)
+	rwMux             = &sync.RWMutex{}
+	currentRules      = make(map[string][]*Rule, 0)
+	updateRuleMux     = new(sync.Mutex)
 )
 
 // LoadRules loads the given isolation rules to the rule manager, while all previous rules will be replaced.
@@ -55,29 +58,40 @@ func LoadRules(rules []*Rule) (bool, error) {
 }
 
 func onRuleUpdate(rawResRulesMap map[string][]*Rule) (err error) {
-	validResRulesMap := make(map[string][]*Rule, len(rawResRulesMap))
+	validResRulesMap := make(map[string][]*Rule)
+	validRegexResRulesMap := make(map[string][]*Rule)
 	for res, rules := range rawResRulesMap {
-		validResRules := make([]*Rule, 0, len(rules))
+		validResRules := make([]*Rule, 0)
+		validRegexResRules := make([]*Rule, 0)
 		for _, rule := range rules {
 			if err := IsValidRule(rule); err != nil {
 				logging.Warn("[Isolation onRuleUpdate] Ignoring invalid isolation rule", "rule", rule, "reason", err.Error())
 				continue
 			}
-			validResRules = append(validResRules, rule)
+			if rule.Regex {
+				validRegexResRules = append(validRegexResRules, rule)
+			} else {
+				validResRules = append(validResRules, rule)
+			}
 		}
 		if len(validResRules) > 0 {
 			validResRulesMap[res] = validResRules
 		}
+		if len(validRegexResRules) > 0 {
+			validRegexResRulesMap[res] = validRegexResRules
+		}
 	}
 
 	start := util.CurrentTimeNano()
 	rwMux.Lock()
 	ruleMap = validResRulesMap
+	regexRuleMap = validRegexResRulesMap
+	regexCacheRuleMap = make(map[string][]*Rule)
 	rwMux.Unlock()
 	currentRules = rawResRulesMap
 
 	logging.Debug("[Isolation onRuleUpdate] Time statistic(ns) for updating isolation rule", "timeCost", util.CurrentTimeNano()-start)
-	logRuleUpdate(validResRulesMap)
+	logRuleUpdate(validResRulesMap, validRegexResRulesMap)
 	return
 }
 
@@ -96,6 +110,8 @@ func LoadRulesOfResource(res string, rules []*Rule) (bool, error) {
 		// clear ruleMap
 		rwMux.Lock()
 		delete(ruleMap, res)
+		delete(regexRuleMap, res)
+		regexCacheRuleMap = make(map[string][]*Rule)
 		rwMux.Unlock()
 		logging.Info("[Isolation] clear resource level rules", "resource", res)
 		return true, nil
@@ -112,22 +128,30 @@ func LoadRulesOfResource(res string, rules []*Rule) (bool, error) {
 }
 
 func onResourceRuleUpdate(res string, rawResRules []*Rule) (err error) {
-	validResRules := make([]*Rule, 0, len(rawResRules))
+	validResRules := make([]*Rule, 0)
+	validRegexResRules := make([]*Rule, 0)
 	for _, rule := range rawResRules {
 		if err := IsValidRule(rule); err != nil {
 			logging.Warn("[Isolation onResourceRuleUpdate] Ignoring invalid isolation rule", "rule", rule, "reason", err.Error())
 			continue
 		}
-		validResRules = append(validResRules, rule)
+		if rule.Regex {
+			validRegexResRules = append(validRegexResRules, rule)
+		} else {
+			validResRules = append(validResRules, rule)
+		}
 	}
 
 	start := util.CurrentTimeNano()
 	rwMux.Lock()
 	if len(validResRules) == 0 {
 		delete(ruleMap, res)
+		delete(regexRuleMap, res)
 	} else {
 		ruleMap[res] = validResRules
+		regexRuleMap[res] = validRegexResRules
 	}
+	regexCacheRuleMap = make(map[string][]*Rule)
 	rwMux.Unlock()
 	currentRules[res] = rawResRules
 	logging.Debug("[Isolation onResourceRuleUpdate] Time statistic(ns) for updating isolation rule", "timeCost", util.CurrentTimeNano()-start)
@@ -182,12 +206,28 @@ func getRules() []*Rule {
 // getRulesOfResource is an internal interface.
 func getRulesOfResource(res string) []*Rule {
 	rwMux.RLock()
-	defer rwMux.RUnlock()
+	resRules := make([]*Rule, 0)
+	if rules, ok := ruleMap[res]; ok {
+		resRules = append(resRules, rules...)
+	}
+	regexCacheRules, ok := regexCacheRuleMap[res]
+	rwMux.RUnlock()
 
-	resRules, exist := ruleMap[res]
-	if !exist {
-		return nil
+	if ok {
+		resRules = append(resRules, regexCacheRules...)
+	} else {
+		rwMux.Lock()
+		// double check
+		if regexCacheRules, ok = regexCacheRuleMap[res]; ok {
+			resRules = append(resRules, regexCacheRules...)
+		} else {
+			regexRules := regexRuleMatches(res)
+			regexCacheRuleMap[res] = regexRules
+			resRules = append(resRules, regexRules...)
+		}
+		rwMux.Unlock()
 	}
+
 	ret := make([]*Rule, 0, len(resRules))
 	for _, r := range resRules {
 		ret = append(ret, r)
@@ -210,13 +250,19 @@ func rulesFrom(m map[string][]*Rule) []*Rule {
 	return rules
 }
 
-func logRuleUpdate(m map[string][]*Rule) {
+func logRuleUpdate(m map[string][]*Rule, regexM map[string][]*Rule) {
 	rs := rulesFrom(m)
 	if len(rs) == 0 {
 		logging.Info("[IsolationRuleManager] Isolation rules were cleared")
 	} else {
 		logging.Info("[IsolationRuleManager] Isolation rules were loaded", "rules", rs)
 	}
+	reRs := rulesFrom(regexM)
+	if len(reRs) == 0 {
+		logging.Info("[IsolationRuleManager] Isolation regex rules were cleared")
+	} else {
+		logging.Info("[IsolationRuleManager] Isolation regex rules were loaded", "rules", reRs)
+	}
 }
 
 // IsValidRule checks whether the given Rule is valid.
@@ -235,3 +281,17 @@ func IsValidRule(r *Rule) error {
 	}
 	return nil
 }
+
+func regexRuleMatches(resource string) []*Rule {
+	result := make([]*Rule, 0)
+	for pattern, regexRules := range regexRuleMap {
+		re := regexp.MustCompile(pattern)
+		if !re.MatchString(resource) {
+			continue
+		}
+		for _, rule := range regexRules {
+			result = append(result, rule)
+		}
+	}
+	return result
+}


Test patch:
diff --git a/core/flow/rule_manager_test.go b/core/flow/rule_manager_test.go
index 3025aae..6079bc1 100644
--- a/core/flow/rule_manager_test.go
+++ b/core/flow/rule_manager_test.go
@@ -263,7 +263,7 @@ func Test_buildResourceTrafficShapingController(t *testing.T) {
 			MaxQueueingTimeMs:      10,
 		}
 		assert.True(t, len(tcMap["abc1"]) == 0)
-		tcs := buildResourceTrafficShapingController("abc1", []*Rule{r1, r2}, tcMap["abc1"])
+		tcs, _ := buildResourceTrafficShapingController("abc1", []*Rule{r1, r2}, tcMap["abc1"])
 		assert.True(t, len(tcs) == 2)
 		assert.True(t, tcs[0].BoundRule() == r1)
 		assert.True(t, tcs[1].BoundRule() == r2)
@@ -415,7 +415,7 @@ func Test_buildResourceTrafficShapingController(t *testing.T) {
 			StatIntervalInMs:       50000,
 		}
 
-		tcs := buildResourceTrafficShapingController("abc1", []*Rule{r12, r22, r32, r42}, tcMap["abc1"])
+		tcs, _ := buildResourceTrafficShapingController("abc1", []*Rule{r12, r22, r32, r42}, tcMap["abc1"])
 		assert.True(t, len(tcs) == 4)
 
 		assert.True(t, tcs[0].BoundRule() == r12)
diff --git a/core/flow/slot_test.go b/core/flow/slot_test.go
index b5dacc4..6774a7e 100644
--- a/core/flow/slot_test.go
+++ b/core/flow/slot_test.go
@@ -66,3 +66,68 @@ func Test_FlowSlot_StandaloneStat(t *testing.T) {
 	}
 	assert.True(t, getTrafficControllerListFor("abc")[0].boundStat.readOnlyMetric.GetSum(base.MetricEventPass) == 50)
 }
+
+func Test_RegexFlowSlot_StandaloneStat(t *testing.T) {
+	slot := &Slot{}
+	statSLot := &StandaloneStatSlot{}
+	res1 := base.NewResourceWrapper("abc/123", base.ResTypeCommon, base.Inbound)
+	res2 := base.NewResourceWrapper("abc/456", base.ResTypeCommon, base.Inbound)
+	resNode1 := stat.GetOrCreateResourceNode("abc/123", base.ResTypeCommon)
+	resNode2 := stat.GetOrCreateResourceNode("abc/456", base.ResTypeCommon)
+	ctx1 := &base.EntryContext{
+		Resource: res1,
+		StatNode: resNode1,
+		Input: &base.SentinelInput{
+			BatchCount: 1,
+		},
+		RuleCheckResult: nil,
+		Data:            nil,
+	}
+	ctx2 := &base.EntryContext{
+		Resource: res2,
+		StatNode: resNode2,
+		Input: &base.SentinelInput{
+			BatchCount: 1,
+		},
+		RuleCheckResult: nil,
+		Data:            nil,
+	}
+
+	r1 := &Rule{
+		Resource:               "abc/*",
+		TokenCalculateStrategy: Direct,
+		ControlBehavior:        Reject,
+		// Use standalone statistic, using single-bucket-sliding-windows
+		StatIntervalInMs: 20000,
+		Threshold:        100,
+		RelationStrategy: CurrentResource,
+		Regex:            true,
+	}
+	_, e := LoadRules([]*Rule{r1})
+	if e != nil {
+		logging.Error(e, "")
+		t.Fail()
+		return
+	}
+
+	for i := 0; i < 80; i++ {
+		ret := slot.Check(ctx1)
+		if ret != nil {
+			t.Fail()
+			return
+		}
+		statSLot.OnEntryPassed(ctx1)
+	}
+	for i := 0; i < 80; i++ {
+		ret := slot.Check(ctx2)
+		if ret != nil {
+			t.Fail()
+			return
+		}
+		statSLot.OnEntryPassed(ctx2)
+	}
+	assert.True(t, getTrafficControllerListFor("abc/123")[0].boundStat.readOnlyMetric.GetSum(base.
+		MetricEventPass) == 80)
+	assert.True(t, getTrafficControllerListFor("abc/456")[0].boundStat.readOnlyMetric.GetSum(base.
+		MetricEventPass) == 80)
+}
diff --git a/core/isolation/slot_test.go b/core/isolation/slot_test.go
new file mode 100644
index 0000000..fd43394
--- /dev/null
+++ b/core/isolation/slot_test.go
@@ -0,0 +1,155 @@
+package isolation
+
+import (
+	"testing"
+
+	"github.com/alibaba/sentinel-golang/core/base"
+	"github.com/alibaba/sentinel-golang/core/stat"
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_Isolation_Pass(t *testing.T) {
+	slot := &Slot{}
+	statSlot := stat.Slot{}
+	res := base.NewResourceWrapper("abc", base.ResTypeCommon, base.Inbound)
+	resNode := stat.GetOrCreateResourceNode("abc", base.ResTypeCommon)
+	ctx := &base.EntryContext{
+		Resource: res,
+		StatNode: resNode,
+		Input: &base.SentinelInput{
+			BatchCount: 1,
+		},
+		RuleCheckResult: nil,
+		Data:            nil,
+	}
+
+	ret := slot.Check(ctx)
+	if ret != nil {
+		t.Fail()
+		return
+	}
+
+	r := &Rule{
+		Resource:   "abc",
+		MetricType: Concurrency,
+		Threshold:  10,
+	}
+	_, e := LoadRules([]*Rule{r})
+	if e != nil {
+		t.Fail()
+		return
+	}
+	for i := 0; i < 10; i++ {
+		ret = slot.Check(ctx)
+		if ret != nil {
+			t.Fail()
+			return
+		}
+		statSlot.OnEntryPassed(ctx)
+	}
+
+	ret = slot.Check(ctx)
+	assert.NotNil(t, ret)
+}
+
+func Test_RegexIsolation_Pass(t *testing.T) {
+	slot := &Slot{}
+	statSlot1 := stat.Slot{}
+	statSlot2 := stat.Slot{}
+	res1 := base.NewResourceWrapper("abc/123", base.ResTypeCommon, base.Inbound)
+	res2 := base.NewResourceWrapper("abc/456", base.ResTypeCommon, base.Inbound)
+	resNode1 := stat.GetOrCreateResourceNode("abc/123", base.ResTypeCommon)
+	resNode2 := stat.GetOrCreateResourceNode("abc/456", base.ResTypeCommon)
+	ctx1 := &base.EntryContext{
+		Resource: res1,
+		StatNode: resNode1,
+		Input: &base.SentinelInput{
+			BatchCount: 1,
+		},
+		RuleCheckResult: nil,
+		Data:            nil,
+	}
+	ctx2 := &base.EntryContext{
+		Resource: res2,
+		StatNode: resNode2,
+		Input: &base.SentinelInput{
+			BatchCount: 1,
+		},
+		RuleCheckResult: nil,
+		Data:            nil,
+	}
+
+	ret1 := slot.Check(ctx1)
+	if ret1 != nil {
+		t.Fail()
+		return
+	}
+	ret2 := slot.Check(ctx2)
+	if ret2 != nil {
+		t.Fail()
+		return
+	}
+
+	r := &Rule{
+		Resource:   "abc/\\d+",
+		MetricType: Concurrency,
+		Threshold:  10,
+		Regex:      true,
+	}
+	_, e := LoadRules([]*Rule{r})
+	if e != nil {
+		t.Fail()
+		return
+	}
+
+	n := 10
+	for i := 0; i < n; i++ {
+		ret := slot.Check(ctx1)
+		if ret != nil {
+			t.Fail()
+			return
+		}
+
+		statSlot1.OnEntryPassed(ctx1)
+	}
+
+	ret1 = slot.Check(ctx1)
+	assert.NotNil(t, ret1)
+
+	for i := 0; i < n; i++ {
+		statSlot1.OnCompleted(ctx1)
+	}
+
+	for i := 0; i < n; i++ {
+		ret := slot.Check(ctx1)
+		if ret != nil {
+			t.Fail()
+			return
+		}
+		statSlot1.OnEntryPassed(ctx1)
+	}
+
+	for i := 0; i < n; i++ {
+		ret := slot.Check(ctx2)
+		if ret != nil {
+			t.Fail()
+			return
+		}
+		statSlot2.OnEntryPassed(ctx2)
+	}
+	ret2 = slot.Check(ctx2)
+	assert.NotNil(t, ret2)
+
+	for i := 0; i < n; i++ {
+		statSlot2.OnCompleted(ctx2)
+	}
+
+	for i := 0; i < n; i++ {
+		ret := slot.Check(ctx2)
+		if ret != nil {
+			t.Fail()
+			return
+		}
+		statSlot2.OnEntryPassed(ctx2)
+	}
+}
diff --git a/ext/datasource/helper_test.go b/ext/datasource/helper_test.go
index becdd88..3a4daf7 100644
--- a/ext/datasource/helper_test.go
+++ b/ext/datasource/helper_test.go
@@ -532,10 +532,10 @@ func TestIsolationRuleJsonArrayParser(t *testing.T) {
 		rules := properties.([]*isolation.Rule)
 		assert.True(t, err == nil)
 		assert.True(t, len(rules) == 4)
-		assert.True(t, strings.Contains(rules[0].String(), `{"resource":"abc","metricType":0,"threshold":100}`))
-		assert.True(t, strings.Contains(rules[1].String(), `{"resource":"abc","metricType":0,"threshold":90}`))
-		assert.True(t, strings.Contains(rules[2].String(), `{"resource":"abc","metricType":0,"threshold":80}`))
-		assert.True(t, strings.Contains(rules[3].String(), `{"resource":"abc","metricType":0,"threshold":70}`))
+		assert.True(t, strings.Contains(rules[0].String(), `{"resource":"abc","metricType":0,"threshold":100,"regex":false}`))
+		assert.True(t, strings.Contains(rules[1].String(), `{"resource":"abc","metricType":0,"threshold":90,"regex":false}`))
+		assert.True(t, strings.Contains(rules[2].String(), `{"resource":"abc","metricType":0,"threshold":80,"regex":false}`))
+		assert.True(t, strings.Contains(rules[3].String(), `{"resource":"abc","metricType":0,"threshold":70,"regex":false}`))
 	})
 
 	t.Run("TestIsolationRuleJsonArrayParser_Nil", func(t *testing.T) {