Skip to content

Commit dd1b92f

Browse files
committed
Add unit tests for rule_manager.go
1 parent 67796fb commit dd1b92f

File tree

3 files changed

+322
-5
lines changed

3 files changed

+322
-5
lines changed

core/outlier/retryer.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ func (r *Retryer) onDisconnected(node string) {
130130
count = r.maxAttempts
131131
}
132132
r.mtx.Unlock()
133+
// Fix bugs: When multiple active checks still do not recover, it is necessary to delete node from r.counts.
133134
time.AfterFunc(r.interval*time.Duration(count), func() {
134135
r.connectNode(node)
135136
})

core/outlier/rule_manager.go

Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ var (
3333
// resource name ---> address ---> circuitbreaker
3434
nodeBreakers = make(map[string]map[string]circuitbreaker.CircuitBreaker)
3535
// resource name ---> outlier ejection rule
36-
currentRules = make(map[string]*Rule)
37-
updateMux = new(sync.RWMutex)
36+
currentRules = make(map[string]*Rule)
37+
updateMux = new(sync.RWMutex)
38+
updateRuleMux = new(sync.Mutex)
3839
)
3940

4041
func getNodeBreakersOfResource(resource string) map[string]circuitbreaker.CircuitBreaker {
@@ -131,6 +132,8 @@ func LoadRules(rules []*Rule) (bool, error) {
131132
for _, rule := range rules {
132133
rulesMap[rule.Resource] = rule
133134
}
135+
updateRuleMux.Lock()
136+
defer updateRuleMux.Unlock()
134137
isEqual := reflect.DeepEqual(currentRules, rulesMap)
135138
if isEqual {
136139
logging.Info("[Outlier] Load rules is the same with current rules, so ignore load operation.")
@@ -140,6 +143,79 @@ func LoadRules(rules []*Rule) (bool, error) {
140143
return true, err
141144
}
142145

146+
// LoadRuleOfResource loads the given resource's outlier ejection rule to the rule manager, while previous resource's rule will be replaced.
147+
// the first returned value indicates whether do real load operation, if the rule is the same with previous resource's rule, return false
148+
func LoadRuleOfResource(res string, rule *Rule) (bool, error) {
149+
if len(res) == 0 {
150+
return false, errors.New("empty resource")
151+
}
152+
updateRuleMux.Lock()
153+
defer updateRuleMux.Unlock()
154+
// clear resource rule
155+
if rule == nil {
156+
delete(currentRules, res)
157+
updateMux.Lock()
158+
delete(nodeBreakers, res)
159+
delete(breakerRules, res)
160+
delete(outlierRules, res)
161+
updateMux.Unlock()
162+
logging.Info("[Outlier] clear resource level rule", "resource", res)
163+
return true, nil
164+
}
165+
// load resource level rule
166+
isEqual := reflect.DeepEqual(currentRules[res], rule)
167+
if isEqual {
168+
logging.Info("[Outlier] Load resource level rule is the same with current resource level rule, so ignore load operation.")
169+
return false, nil
170+
}
171+
err := onResourceRuleUpdate(res, rule)
172+
return true, err
173+
}
174+
175+
func onResourceRuleUpdate(res string, rule *Rule) (err error) {
176+
defer func() {
177+
if r := recover(); r != nil {
178+
var ok bool
179+
err, ok = r.(error)
180+
if !ok {
181+
err = fmt.Errorf("%v", r)
182+
}
183+
}
184+
}()
185+
186+
circuitRule := rule.Rule
187+
if err = IsValidRule(rule); err != nil {
188+
logging.Warn("[Outlier onResourceRuleUpdate] Ignoring invalid outlier ejection rule", "rule", rule, "err", err.Error())
189+
return
190+
}
191+
if err = circuitbreaker.IsValidRule(circuitRule); err != nil {
192+
logging.Warn("[Outlier onRuleUpdate] Ignoring invalid rule when loading new rules", "rule", rule, "err", err.Error())
193+
return
194+
}
195+
196+
start := util.CurrentTimeNano()
197+
breakers := getNodeBreakersOfResource(res)
198+
newBreakers := make(map[string]circuitbreaker.CircuitBreaker)
199+
for address, breaker := range breakers {
200+
newCbsOfRes := circuitbreaker.BuildResourceCircuitBreaker(res,
201+
[]*circuitbreaker.Rule{circuitRule}, []circuitbreaker.CircuitBreaker{breaker})
202+
if len(newCbsOfRes) > 0 {
203+
newBreakers[address] = newCbsOfRes[0]
204+
}
205+
}
206+
207+
updateMux.Lock()
208+
outlierRules[res] = rule
209+
breakerRules[res] = circuitRule
210+
nodeBreakers[res] = newBreakers
211+
updateMux.Unlock()
212+
currentRules[res] = rule
213+
214+
logging.Debug("[Outlier onResourceRuleUpdate] Time statistics(ns) for updating outlier ejection rule", "timeCost", util.CurrentTimeNano()-start)
215+
logging.Info("[Outlier] load resource level rule", "resource", res, "rule", rule)
216+
return nil
217+
}
218+
143219
// onRuleUpdate is concurrent safe to update outlier ejection rules
144220
func onRuleUpdate(rulesMap map[string]*Rule) (err error) {
145221
defer func() {
@@ -157,9 +233,11 @@ func onRuleUpdate(rulesMap map[string]*Rule) (err error) {
157233
validRulesMap := make(map[string]*Rule, len(rulesMap))
158234
for resource, rule := range rulesMap {
159235
circuitRule := rule.Rule
160-
err := IsValidRule(rule)
161-
err = circuitbreaker.IsValidRule(circuitRule)
162-
if err != nil {
236+
if err = IsValidRule(rule); err != nil {
237+
logging.Warn("[Outlier onRuleUpdate] Ignoring invalid rule when loading new rules", "rule", rule, "err", err.Error())
238+
continue
239+
}
240+
if err = circuitbreaker.IsValidRule(circuitRule); err != nil {
163241
logging.Warn("[Outlier onRuleUpdate] Ignoring invalid rule when loading new rules", "rule", rule, "err", err.Error())
164242
continue
165243
}
@@ -178,6 +256,12 @@ func onRuleUpdate(rulesMap map[string]*Rule) (err error) {
178256
return nil
179257
}
180258

259+
// ClearRuleOfResource clears resource level rule in outlier ejection module.
260+
func ClearRuleOfResource(res string) error {
261+
_, err := LoadRuleOfResource(res, nil)
262+
return err
263+
}
264+
181265
func IsValidRule(r *Rule) error {
182266
if r == nil {
183267
return errors.New("nil Rule")

core/outlier/rule_manager_test.go

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
// Copyright 1999-2020 Alibaba Group Holding Ltd.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
package outlier
15+
16+
import (
17+
"testing"
18+
19+
"github.com/stretchr/testify/assert"
20+
21+
"github.com/alibaba/sentinel-golang/core/circuitbreaker"
22+
)
23+
24+
func clearData() {
25+
// resource name ---> outlier ejection rule
26+
outlierRules = make(map[string]*Rule)
27+
// resource name ---> circuitbreaker rule
28+
breakerRules = make(map[string]*circuitbreaker.Rule)
29+
// resource name ---> address ---> circuitbreaker
30+
nodeBreakers = make(map[string]map[string]circuitbreaker.CircuitBreaker)
31+
// resource name ---> outlier ejection rule
32+
currentRules = make(map[string]*Rule)
33+
}
34+
35+
func Test_onRuleUpdateInvalid(t *testing.T) {
36+
r1 := &Rule{
37+
Rule: &circuitbreaker.Rule{
38+
Resource: "example.helloworld",
39+
Strategy: circuitbreaker.ErrorCount,
40+
RetryTimeoutMs: 3000,
41+
MinRequestAmount: 1,
42+
StatIntervalMs: 1000,
43+
Threshold: 1.0,
44+
},
45+
EnableActiveRecovery: true,
46+
MaxEjectionPercent: 1.5, // MaxEjectionPercent should be in the range [0.0, 1.0]
47+
RecoveryIntervalMs: 2000,
48+
MaxRecoveryAttempts: 5,
49+
}
50+
resRulesMap := make(map[string]*Rule)
51+
resRulesMap[r1.Resource] = r1
52+
err := onRuleUpdate(resRulesMap)
53+
assert.Nil(t, err)
54+
assert.Equal(t, 0, len(GetRules()))
55+
clearData()
56+
}
57+
58+
func TestGetRules(t *testing.T) {
59+
r1 := &Rule{
60+
Rule: &circuitbreaker.Rule{
61+
Resource: "example.helloworld",
62+
Strategy: circuitbreaker.ErrorCount,
63+
RetryTimeoutMs: 3000,
64+
MinRequestAmount: 1,
65+
StatIntervalMs: 1000,
66+
Threshold: 1.0,
67+
},
68+
EnableActiveRecovery: true,
69+
MaxEjectionPercent: 1.0,
70+
RecoveryIntervalMs: 2000,
71+
MaxRecoveryAttempts: 5,
72+
}
73+
_, _ = LoadRules([]*Rule{r1})
74+
rules := GetRules()
75+
assert.True(t, len(rules) == 1 && rules[0].Resource == r1.Resource && rules[0].Strategy == r1.Strategy)
76+
clearData()
77+
}
78+
79+
func TestGetNodeBreakersOfResource(t *testing.T) {
80+
r1 := &Rule{
81+
Rule: &circuitbreaker.Rule{
82+
Resource: "example.helloworld",
83+
Strategy: circuitbreaker.ErrorCount,
84+
RetryTimeoutMs: 3000,
85+
MinRequestAmount: 1,
86+
StatIntervalMs: 1000,
87+
Threshold: 1.0,
88+
},
89+
EnableActiveRecovery: true,
90+
MaxEjectionPercent: 1.0,
91+
RecoveryIntervalMs: 2000,
92+
MaxRecoveryAttempts: 5,
93+
}
94+
_, _ = LoadRules([]*Rule{r1})
95+
addNodeBreakerOfResource(r1.Resource, "node0")
96+
cbs := getNodeBreakersOfResource(r1.Resource)
97+
assert.True(t, len(cbs) == 1 && cbs["node0"].BoundRule() == r1.Rule)
98+
clearData()
99+
}
100+
101+
func TestLoadRules(t *testing.T) {
102+
r1 := &Rule{
103+
Rule: &circuitbreaker.Rule{
104+
Resource: "example.helloworld",
105+
Strategy: circuitbreaker.ErrorCount,
106+
RetryTimeoutMs: 3000,
107+
MinRequestAmount: 1,
108+
StatIntervalMs: 1000,
109+
Threshold: 1.0,
110+
},
111+
EnableActiveRecovery: true,
112+
MaxEjectionPercent: 1.0,
113+
RecoveryIntervalMs: 2000,
114+
MaxRecoveryAttempts: 5,
115+
}
116+
_, err := LoadRules([]*Rule{r1})
117+
assert.Nil(t, err)
118+
ok, err := LoadRules([]*Rule{r1})
119+
assert.Nil(t, err)
120+
assert.False(t, ok)
121+
clearData()
122+
}
123+
124+
func getTestRules() []*Rule {
125+
r1 := &Rule{
126+
Rule: &circuitbreaker.Rule{
127+
Resource: "example.helloworld",
128+
Strategy: circuitbreaker.SlowRequestRatio,
129+
RetryTimeoutMs: 3000,
130+
MinRequestAmount: 1,
131+
StatIntervalMs: 1000,
132+
Threshold: 1.0,
133+
},
134+
EnableActiveRecovery: true,
135+
MaxEjectionPercent: 1.0,
136+
RecoveryIntervalMs: 2000,
137+
MaxRecoveryAttempts: 5,
138+
}
139+
r2 := &Rule{
140+
Rule: &circuitbreaker.Rule{
141+
Resource: "example.helloworld",
142+
Strategy: circuitbreaker.ErrorRatio,
143+
RetryTimeoutMs: 3000,
144+
MinRequestAmount: 1,
145+
StatIntervalMs: 1000,
146+
Threshold: 1.0,
147+
},
148+
EnableActiveRecovery: true,
149+
MaxEjectionPercent: 1.0,
150+
RecoveryIntervalMs: 2000,
151+
MaxRecoveryAttempts: 5,
152+
}
153+
r3 := &Rule{
154+
Rule: &circuitbreaker.Rule{
155+
Resource: "test.resource",
156+
Strategy: circuitbreaker.ErrorCount,
157+
RetryTimeoutMs: 3000,
158+
MinRequestAmount: 1,
159+
StatIntervalMs: 1000,
160+
Threshold: 10.0,
161+
},
162+
EnableActiveRecovery: true,
163+
MaxEjectionPercent: 1.0,
164+
RecoveryIntervalMs: 2000,
165+
MaxRecoveryAttempts: 5,
166+
}
167+
return []*Rule{r1, r2, r3}
168+
}
169+
170+
func TestLoadRuleOfResource(t *testing.T) {
171+
rules := getTestRules()
172+
r1, r2, _ := rules[0], rules[1], rules[2]
173+
succ, err := LoadRules(rules)
174+
assert.Equal(t, 2, len(breakerRules))
175+
assert.True(t, succ && err == nil)
176+
177+
t.Run("LoadRuleOfResource_empty_resource", func(t *testing.T) {
178+
succ, err = LoadRuleOfResource("", r1)
179+
assert.True(t, !succ && err != nil)
180+
})
181+
182+
t.Run("LoadRuleOfResource_cache_hit", func(t *testing.T) {
183+
assert.Equal(t, r2, getOutlierRuleOfResource("example.helloworld"))
184+
succ, err = LoadRuleOfResource("example.helloworld", r1)
185+
assert.True(t, succ && err == nil)
186+
})
187+
188+
t.Run("LoadRuleOfResource_clear", func(t *testing.T) {
189+
succ, err = LoadRuleOfResource("example.helloworld", nil)
190+
assert.Equal(t, 1, len(breakerRules))
191+
assert.True(t, succ && err == nil)
192+
assert.True(t, breakerRules["example.helloworld"] == nil && currentRules["example.helloworld"] == nil)
193+
assert.True(t, breakerRules["test.resource"] != nil && currentRules["test.resource"] != nil)
194+
})
195+
clearData()
196+
}
197+
198+
func Test_onResourceRuleUpdate(t *testing.T) {
199+
rules := getTestRules()
200+
r1 := rules[0]
201+
succ, err := LoadRules(rules)
202+
addNodeBreakerOfResource(r1.Resource, "node0")
203+
assert.True(t, succ && err == nil)
204+
205+
t.Run("Test_onResourceRuleUpdate_normal", func(t *testing.T) {
206+
r11 := r1
207+
r11.Threshold = 0.5
208+
assert.Nil(t, onResourceRuleUpdate(r1.Resource, r11))
209+
assert.Equal(t, getOutlierRuleOfResource(r1.Resource), r11)
210+
assert.Equal(t, 1, len(nodeBreakers[r1.Resource]))
211+
breakers := getNodeBreakersOfResource(r1.Resource)
212+
assert.Equal(t, breakers["node0"].BoundRule(), r11.Rule)
213+
clearData()
214+
})
215+
}
216+
217+
func TestClearRuleOfResource(t *testing.T) {
218+
rules := getTestRules()
219+
r1 := rules[0]
220+
succ, err := LoadRules(rules)
221+
addNodeBreakerOfResource(r1.Resource, "node0")
222+
assert.True(t, succ && err == nil)
223+
224+
t.Run("TestClearRuleOfResource_normal", func(t *testing.T) {
225+
assert.Equal(t, 1, len(nodeBreakers[r1.Resource]))
226+
assert.Nil(t, ClearRuleOfResource(r1.Resource))
227+
assert.Equal(t, 1, len(breakerRules))
228+
assert.Equal(t, 0, len(nodeBreakers[r1.Resource]))
229+
assert.Equal(t, 1, len(currentRules))
230+
clearData()
231+
})
232+
}

0 commit comments

Comments
 (0)