Skip to content

Commit f8351f6

Browse files
YoshiyukiMineocall-stackKalpit Pant
authoredDec 28, 2024··
Title: Implement Distributed Circuit Breaker (#70) (#73)
* Title: Implement Distributed Circuit Breaker (#70) * feature/redis-circuit-breaker * feature/redis-circuit-breaker * Refactor * save state * Saving half-open state also * Saving half-open state also * Added test case * Saving state transition * Pass context * Moved redis circuit breaker to v2 * Revert go.mod and go.sum * Acked review comments * Refactor * Refactor --------- Co-authored-by: Kalpit Pant <kalpit@setu.co> * Rename * Rename * Refactor * Rename * Rename * Use generic cache store (#74) Co-authored-by: Kalpit Pant <kalpit@setu.co> * Update distributed_gobreaker.go * Update distributed_gobreaker.go * Update distributed_gobreaker.go * Update distributed_gobreaker_test.go * Update distributed_gobreaker_test.go * Update distributed_gobreaker_test.go * Update distributed_gobreaker_test.go * Update distributed_gobreaker_test.go * Update distributed_gobreaker_test.go * Update distributed_gobreaker_test.go * Update distributed_gobreaker_test.go * Update distributed_gobreaker_test.go * Update distributed_gobreaker_test.go * Update distributed_gobreaker_test.go * Update distributed_gobreaker.go * Update distributed_gobreaker_test.go * Update distributed_gobreaker_test.go * Update distributed_gobreaker_test.go --------- Co-authored-by: kp <kalpitpant32@gmail.com> Co-authored-by: Kalpit Pant <kalpit@setu.co>
1 parent d78b227 commit f8351f6

File tree

4 files changed

+631
-0
lines changed

4 files changed

+631
-0
lines changed
 

‎v2/distributed_gobreaker.go

+265
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
package gobreaker
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"time"
8+
)
9+
10+
var (
11+
// ErrNoSharedStore is returned when there is no shared store.
12+
ErrNoSharedStore = errors.New("no shared store")
13+
// ErrNoSharedState is returned when there is no shared state.
14+
ErrNoSharedState = errors.New("no shared state")
15+
)
16+
17+
// SharedState represents the shared state of DistributedCircuitBreaker.
18+
type SharedState struct {
19+
State State `json:"state"`
20+
Generation uint64 `json:"generation"`
21+
Counts Counts `json:"counts"`
22+
Expiry time.Time `json:"expiry"`
23+
}
24+
25+
// SharedDataStore stores the shared state of DistributedCircuitBreaker.
26+
type SharedDataStore interface {
27+
GetData(ctx context.Context, name string) ([]byte, error)
28+
SetData(ctx context.Context, name string, data []byte) error
29+
}
30+
31+
// DistributedCircuitBreaker extends CircuitBreaker with SharedDataStore.
32+
type DistributedCircuitBreaker[T any] struct {
33+
*CircuitBreaker[T]
34+
store SharedDataStore
35+
}
36+
37+
// NewDistributedCircuitBreaker returns a new DistributedCircuitBreaker.
38+
func NewDistributedCircuitBreaker[T any](ctx context.Context, store SharedDataStore, settings Settings) (*DistributedCircuitBreaker[T], error) {
39+
if store == nil {
40+
return nil, ErrNoSharedStore
41+
}
42+
43+
dcb := &DistributedCircuitBreaker[T]{
44+
CircuitBreaker: NewCircuitBreaker[T](settings),
45+
store: store,
46+
}
47+
state := SharedState{
48+
State: dcb.state,
49+
Generation: dcb.generation,
50+
Counts: dcb.counts,
51+
Expiry: dcb.expiry,
52+
}
53+
err := dcb.setSharedState(ctx, state)
54+
return dcb, err
55+
}
56+
57+
func (dcb *DistributedCircuitBreaker[T]) sharedStateKey() string {
58+
return "gobreaker:" + dcb.name
59+
}
60+
61+
func (dcb *DistributedCircuitBreaker[T]) getSharedState(ctx context.Context) (SharedState, error) {
62+
var state SharedState
63+
if dcb.store == nil {
64+
return state, ErrNoSharedStore
65+
}
66+
67+
data, err := dcb.store.GetData(ctx, dcb.sharedStateKey())
68+
if len(data) == 0 {
69+
return state, ErrNoSharedState
70+
} else if err != nil {
71+
return state, err
72+
}
73+
74+
err = json.Unmarshal(data, &state)
75+
return state, err
76+
}
77+
78+
func (dcb *DistributedCircuitBreaker[T]) setSharedState(ctx context.Context, state SharedState) error {
79+
if dcb.store == nil {
80+
return ErrNoSharedStore
81+
}
82+
83+
data, err := json.Marshal(state)
84+
if err != nil {
85+
return err
86+
}
87+
88+
return dcb.store.SetData(ctx, dcb.sharedStateKey(), data)
89+
}
90+
91+
// State returns the State of DistributedCircuitBreaker.
92+
func (dcb *DistributedCircuitBreaker[T]) State(ctx context.Context) (State, error) {
93+
state, err := dcb.getSharedState(ctx)
94+
if err != nil {
95+
return state.State, err
96+
}
97+
98+
now := time.Now()
99+
currentState, _ := dcb.currentState(state, now)
100+
101+
// update the state if it has changed
102+
if currentState != state.State {
103+
state.State = currentState
104+
if err := dcb.setSharedState(ctx, state); err != nil {
105+
return state.State, err
106+
}
107+
}
108+
109+
return state.State, nil
110+
}
111+
112+
// Execute runs the given request if the DistributedCircuitBreaker accepts it.
113+
func (dcb *DistributedCircuitBreaker[T]) Execute(ctx context.Context, req func() (T, error)) (t T, err error) {
114+
generation, err := dcb.beforeRequest(ctx)
115+
if err != nil {
116+
var defaultValue T
117+
return defaultValue, err
118+
}
119+
120+
defer func() {
121+
e := recover()
122+
if e != nil {
123+
ae := dcb.afterRequest(ctx, generation, false)
124+
if err == nil {
125+
err = ae
126+
}
127+
panic(e)
128+
}
129+
}()
130+
131+
result, err := req()
132+
ae := dcb.afterRequest(ctx, generation, dcb.isSuccessful(err))
133+
if err == nil {
134+
err = ae
135+
}
136+
return result, err
137+
}
138+
139+
func (dcb *DistributedCircuitBreaker[T]) beforeRequest(ctx context.Context) (uint64, error) {
140+
state, err := dcb.getSharedState(ctx)
141+
if err != nil {
142+
return 0, err
143+
}
144+
145+
now := time.Now()
146+
currentState, generation := dcb.currentState(state, now)
147+
148+
if currentState != state.State {
149+
dcb.setState(&state, currentState, now)
150+
err = dcb.setSharedState(ctx, state)
151+
if err != nil {
152+
return 0, err
153+
}
154+
}
155+
156+
if currentState == StateOpen {
157+
return generation, ErrOpenState
158+
} else if currentState == StateHalfOpen && state.Counts.Requests >= dcb.maxRequests {
159+
return generation, ErrTooManyRequests
160+
}
161+
162+
state.Counts.onRequest()
163+
err = dcb.setSharedState(ctx, state)
164+
if err != nil {
165+
return 0, err
166+
}
167+
168+
return generation, nil
169+
}
170+
171+
func (dcb *DistributedCircuitBreaker[T]) afterRequest(ctx context.Context, before uint64, success bool) error {
172+
state, err := dcb.getSharedState(ctx)
173+
if err != nil {
174+
return err
175+
}
176+
177+
now := time.Now()
178+
currentState, generation := dcb.currentState(state, now)
179+
if generation != before {
180+
return nil
181+
}
182+
183+
if success {
184+
dcb.onSuccess(&state, currentState, now)
185+
} else {
186+
dcb.onFailure(&state, currentState, now)
187+
}
188+
return dcb.setSharedState(ctx, state)
189+
}
190+
191+
func (dcb *DistributedCircuitBreaker[T]) onSuccess(state *SharedState, currentState State, now time.Time) {
192+
if state.State == StateOpen {
193+
state.State = currentState
194+
}
195+
196+
switch currentState {
197+
case StateClosed:
198+
state.Counts.onSuccess()
199+
case StateHalfOpen:
200+
state.Counts.onSuccess()
201+
if state.Counts.ConsecutiveSuccesses >= dcb.maxRequests {
202+
dcb.setState(state, StateClosed, now)
203+
}
204+
}
205+
}
206+
207+
func (dcb *DistributedCircuitBreaker[T]) onFailure(state *SharedState, currentState State, now time.Time) {
208+
switch currentState {
209+
case StateClosed:
210+
state.Counts.onFailure()
211+
if dcb.readyToTrip(state.Counts) {
212+
dcb.setState(state, StateOpen, now)
213+
}
214+
case StateHalfOpen:
215+
dcb.setState(state, StateOpen, now)
216+
}
217+
}
218+
219+
func (dcb *DistributedCircuitBreaker[T]) currentState(state SharedState, now time.Time) (State, uint64) {
220+
switch state.State {
221+
case StateClosed:
222+
if !state.Expiry.IsZero() && state.Expiry.Before(now) {
223+
dcb.toNewGeneration(&state, now)
224+
}
225+
case StateOpen:
226+
if state.Expiry.Before(now) {
227+
dcb.setState(&state, StateHalfOpen, now)
228+
}
229+
}
230+
return state.State, state.Generation
231+
}
232+
233+
func (dcb *DistributedCircuitBreaker[T]) setState(state *SharedState, newState State, now time.Time) {
234+
if state.State == newState {
235+
return
236+
}
237+
238+
prev := state.State
239+
state.State = newState
240+
241+
dcb.toNewGeneration(state, now)
242+
243+
if dcb.onStateChange != nil {
244+
dcb.onStateChange(dcb.name, prev, newState)
245+
}
246+
}
247+
248+
func (dcb *DistributedCircuitBreaker[T]) toNewGeneration(state *SharedState, now time.Time) {
249+
state.Generation++
250+
state.Counts.clear()
251+
252+
var zero time.Time
253+
switch state.State {
254+
case StateClosed:
255+
if dcb.interval == 0 {
256+
state.Expiry = zero
257+
} else {
258+
state.Expiry = now.Add(dcb.interval)
259+
}
260+
case StateOpen:
261+
state.Expiry = now.Add(dcb.timeout)
262+
default: // StateHalfOpen
263+
state.Expiry = zero
264+
}
265+
}

‎v2/distributed_gobreaker_test.go

+345
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
package gobreaker
2+
3+
import (
4+
"context"
5+
"errors"
6+
"testing"
7+
"time"
8+
9+
"github.com/alicebob/miniredis/v2"
10+
"github.com/redis/go-redis/v9"
11+
"github.com/stretchr/testify/assert"
12+
)
13+
14+
type storeAdapter struct {
15+
client *redis.Client
16+
}
17+
18+
func (r *storeAdapter) GetData(ctx context.Context, key string) ([]byte, error) {
19+
return r.client.Get(ctx, key).Bytes()
20+
}
21+
22+
func (r *storeAdapter) SetData(ctx context.Context, key string, value []byte) error {
23+
return r.client.Set(ctx, key, value, 0).Err()
24+
}
25+
26+
var redisServer *miniredis.Miniredis
27+
28+
func setUpDCB(ctx context.Context) *DistributedCircuitBreaker[any] {
29+
var err error
30+
redisServer, err := miniredis.Run()
31+
if err != nil {
32+
panic(err)
33+
}
34+
35+
client := redis.NewClient(&redis.Options{
36+
Addr: redisServer.Addr(),
37+
})
38+
39+
store := &storeAdapter{client: client}
40+
41+
dcb, err := NewDistributedCircuitBreaker[any](ctx, store, Settings{
42+
Name: "TestBreaker",
43+
MaxRequests: 3,
44+
Interval: time.Second,
45+
Timeout: time.Second * 2,
46+
ReadyToTrip: func(counts Counts) bool {
47+
return counts.ConsecutiveFailures > 5
48+
},
49+
})
50+
if err != nil {
51+
panic(err)
52+
}
53+
54+
return dcb
55+
}
56+
57+
func tearDownDCB(dcb *DistributedCircuitBreaker[any]) {
58+
if dcb != nil {
59+
store := dcb.store.(*storeAdapter)
60+
store.client.Close()
61+
store.client = nil
62+
}
63+
64+
if redisServer != nil {
65+
redisServer.Close()
66+
redisServer = nil
67+
}
68+
}
69+
70+
func dcbPseudoSleep(ctx context.Context, dcb *DistributedCircuitBreaker[any], period time.Duration) {
71+
state, err := dcb.getSharedState(ctx)
72+
if err != nil {
73+
panic(err)
74+
}
75+
76+
state.Expiry = state.Expiry.Add(-period)
77+
// Reset counts if the interval has passed
78+
if time.Now().After(state.Expiry) {
79+
state.Counts = Counts{}
80+
}
81+
82+
err = dcb.setSharedState(ctx, state)
83+
if err != nil {
84+
panic(err)
85+
}
86+
}
87+
88+
func successRequest(ctx context.Context, dcb *DistributedCircuitBreaker[any]) error {
89+
_, err := dcb.Execute(ctx, func() (interface{}, error) { return nil, nil })
90+
return err
91+
}
92+
93+
func failRequest(ctx context.Context, dcb *DistributedCircuitBreaker[any]) error {
94+
_, err := dcb.Execute(ctx, func() (interface{}, error) { return nil, errors.New("fail") })
95+
if err != nil && err.Error() == "fail" {
96+
return nil
97+
}
98+
return err
99+
}
100+
101+
func assertState(ctx context.Context, t *testing.T, dcb *DistributedCircuitBreaker[any], expected State) {
102+
state, err := dcb.State(ctx)
103+
assert.Equal(t, expected, state)
104+
assert.NoError(t, err)
105+
}
106+
107+
func TestDistributedCircuitBreakerInitialization(t *testing.T) {
108+
ctx := context.Background()
109+
dcb := setUpDCB(ctx)
110+
defer tearDownDCB(dcb)
111+
112+
assert.Equal(t, "TestBreaker", dcb.Name())
113+
assert.Equal(t, uint32(3), dcb.maxRequests)
114+
assert.Equal(t, time.Second, dcb.interval)
115+
assert.Equal(t, time.Second*2, dcb.timeout)
116+
assert.NotNil(t, dcb.readyToTrip)
117+
118+
assertState(ctx, t, dcb, StateClosed)
119+
}
120+
121+
func TestDistributedCircuitBreakerStateTransitions(t *testing.T) {
122+
ctx := context.Background()
123+
dcb := setUpDCB(ctx)
124+
defer tearDownDCB(dcb)
125+
126+
// Check if initial state is closed
127+
assertState(ctx, t, dcb, StateClosed)
128+
129+
// StateClosed to StateOpen
130+
for i := 0; i < 6; i++ {
131+
assert.NoError(t, failRequest(ctx, dcb))
132+
}
133+
assertState(ctx, t, dcb, StateOpen)
134+
135+
// Ensure requests fail when the circuit is open
136+
err := failRequest(ctx, dcb)
137+
assert.Equal(t, ErrOpenState, err)
138+
139+
// Wait for timeout so that the state will move to half-open
140+
dcbPseudoSleep(ctx, dcb, dcb.timeout)
141+
assertState(ctx, t, dcb, StateHalfOpen)
142+
143+
// StateHalfOpen to StateClosed
144+
for i := 0; i < int(dcb.maxRequests); i++ {
145+
assert.NoError(t, successRequest(ctx, dcb))
146+
}
147+
assertState(ctx, t, dcb, StateClosed)
148+
149+
// StateClosed to StateOpen (again)
150+
for i := 0; i < 6; i++ {
151+
assert.NoError(t, failRequest(ctx, dcb))
152+
}
153+
assertState(ctx, t, dcb, StateOpen)
154+
}
155+
156+
func TestDistributedCircuitBreakerExecution(t *testing.T) {
157+
ctx := context.Background()
158+
dcb := setUpDCB(ctx)
159+
defer tearDownDCB(dcb)
160+
161+
// Test successful execution
162+
result, err := dcb.Execute(ctx, func() (interface{}, error) {
163+
return "success", nil
164+
})
165+
assert.NoError(t, err)
166+
assert.Equal(t, "success", result)
167+
168+
// Test failed execution
169+
_, err = dcb.Execute(ctx, func() (interface{}, error) {
170+
return nil, errors.New("test error")
171+
})
172+
assert.Error(t, err)
173+
assert.Equal(t, "test error", err.Error())
174+
}
175+
176+
func TestDistributedCircuitBreakerCounts(t *testing.T) {
177+
ctx := context.Background()
178+
dcb := setUpDCB(ctx)
179+
defer tearDownDCB(dcb)
180+
181+
for i := 0; i < 5; i++ {
182+
assert.Nil(t, successRequest(ctx, dcb))
183+
}
184+
185+
state, err := dcb.getSharedState(ctx)
186+
assert.Equal(t, Counts{5, 5, 0, 5, 0}, state.Counts)
187+
assert.NoError(t, err)
188+
189+
assert.Nil(t, failRequest(ctx, dcb))
190+
state, err = dcb.getSharedState(ctx)
191+
assert.Equal(t, Counts{6, 5, 1, 0, 1}, state.Counts)
192+
assert.NoError(t, err)
193+
}
194+
195+
var customDCB *DistributedCircuitBreaker[any]
196+
197+
func TestCustomDistributedCircuitBreaker(t *testing.T) {
198+
ctx := context.Background()
199+
200+
mr, err := miniredis.Run()
201+
if err != nil {
202+
panic(err)
203+
}
204+
defer mr.Close()
205+
206+
client := redis.NewClient(&redis.Options{
207+
Addr: mr.Addr(),
208+
})
209+
210+
store := &storeAdapter{client: client}
211+
212+
customDCB, err = NewDistributedCircuitBreaker[any](ctx, store, Settings{
213+
Name: "CustomBreaker",
214+
MaxRequests: 3,
215+
Interval: time.Second * 30,
216+
Timeout: time.Second * 90,
217+
ReadyToTrip: func(counts Counts) bool {
218+
numReqs := counts.Requests
219+
failureRatio := float64(counts.TotalFailures) / float64(numReqs)
220+
return numReqs >= 3 && failureRatio >= 0.6
221+
},
222+
})
223+
assert.NoError(t, err)
224+
225+
t.Run("Initialization", func(t *testing.T) {
226+
assert.Equal(t, "CustomBreaker", customDCB.Name())
227+
assertState(ctx, t, customDCB, StateClosed)
228+
})
229+
230+
t.Run("Counts and State Transitions", func(t *testing.T) {
231+
// Perform 5 successful and 5 failed requests
232+
for i := 0; i < 5; i++ {
233+
assert.NoError(t, successRequest(ctx, customDCB))
234+
assert.NoError(t, failRequest(ctx, customDCB))
235+
}
236+
237+
state, err := customDCB.getSharedState(ctx)
238+
assert.NoError(t, err)
239+
assert.Equal(t, StateClosed, state.State)
240+
assert.Equal(t, Counts{10, 5, 5, 0, 1}, state.Counts)
241+
242+
// Perform one more successful request
243+
assert.NoError(t, successRequest(ctx, customDCB))
244+
state, err = customDCB.getSharedState(ctx)
245+
assert.NoError(t, err)
246+
assert.Equal(t, Counts{11, 6, 5, 1, 0}, state.Counts)
247+
248+
// Simulate time passing to reset counts
249+
dcbPseudoSleep(ctx, customDCB, time.Second*30)
250+
251+
// Perform requests to trigger StateOpen
252+
assert.NoError(t, successRequest(ctx, customDCB))
253+
assert.NoError(t, failRequest(ctx, customDCB))
254+
assert.NoError(t, failRequest(ctx, customDCB))
255+
256+
// Check if the circuit breaker is now open
257+
assertState(ctx, t, customDCB, StateOpen)
258+
259+
state, err = customDCB.getSharedState(ctx)
260+
assert.NoError(t, err)
261+
assert.Equal(t, Counts{0, 0, 0, 0, 0}, state.Counts)
262+
})
263+
264+
t.Run("Timeout and Half-Open State", func(t *testing.T) {
265+
// Simulate timeout to transition to half-open state
266+
dcbPseudoSleep(ctx, customDCB, time.Second*90)
267+
assertState(ctx, t, customDCB, StateHalfOpen)
268+
269+
// Successful requests in half-open state should close the circuit
270+
for i := 0; i < 3; i++ {
271+
assert.NoError(t, successRequest(ctx, customDCB))
272+
}
273+
assertState(ctx, t, customDCB, StateClosed)
274+
})
275+
}
276+
277+
func TestCustomDistributedCircuitBreakerStateTransitions(t *testing.T) {
278+
// Setup
279+
var stateChange StateChange
280+
customSt := Settings{
281+
Name: "cb",
282+
MaxRequests: 3,
283+
Interval: 5 * time.Second,
284+
Timeout: 5 * time.Second,
285+
ReadyToTrip: func(counts Counts) bool {
286+
return counts.ConsecutiveFailures >= 2
287+
},
288+
OnStateChange: func(name string, from State, to State) {
289+
stateChange = StateChange{name, from, to}
290+
},
291+
}
292+
293+
ctx := context.Background()
294+
295+
mr, err := miniredis.Run()
296+
if err != nil {
297+
t.Fatalf("Failed to start miniredis: %v", err)
298+
}
299+
defer mr.Close()
300+
301+
client := redis.NewClient(&redis.Options{
302+
Addr: mr.Addr(),
303+
})
304+
305+
store := &storeAdapter{client: client}
306+
307+
dcb, err := NewDistributedCircuitBreaker[any](ctx, store, customSt)
308+
assert.NoError(t, err)
309+
310+
// Test case
311+
t.Run("Circuit Breaker State Transitions", func(t *testing.T) {
312+
// Initial state should be Closed
313+
assertState(ctx, t, dcb, StateClosed)
314+
315+
// Cause two consecutive failures to trip the circuit
316+
for i := 0; i < 2; i++ {
317+
err := failRequest(ctx, dcb)
318+
assert.NoError(t, err, "Fail request should not return an error")
319+
}
320+
321+
// Circuit should now be Open
322+
assertState(ctx, t, dcb, StateOpen)
323+
assert.Equal(t, StateChange{"cb", StateClosed, StateOpen}, stateChange)
324+
325+
// Requests should fail immediately when circuit is Open
326+
err := successRequest(ctx, dcb)
327+
assert.Error(t, err)
328+
assert.Equal(t, ErrOpenState, err)
329+
330+
// Simulate timeout to transition to Half-Open
331+
dcbPseudoSleep(ctx, dcb, 6*time.Second)
332+
assertState(ctx, t, dcb, StateHalfOpen)
333+
assert.Equal(t, StateChange{"cb", StateOpen, StateHalfOpen}, stateChange)
334+
335+
// Successful requests in Half-Open state should close the circuit
336+
for i := 0; i < int(dcb.maxRequests); i++ {
337+
err := successRequest(ctx, dcb)
338+
assert.NoError(t, err)
339+
}
340+
341+
// Circuit should now be Closed
342+
assertState(ctx, t, dcb, StateClosed)
343+
assert.Equal(t, StateChange{"cb", StateHalfOpen, StateClosed}, stateChange)
344+
})
345+
}

‎v2/go.mod

+9
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,16 @@ go 1.21
55
require github.com/stretchr/testify v1.8.4
66

77
require (
8+
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect
9+
github.com/cespare/xxhash/v2 v2.2.0 // indirect
10+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
11+
github.com/yuin/gopher-lua v1.1.1 // indirect
12+
)
13+
14+
require (
15+
github.com/alicebob/miniredis/v2 v2.33.0
816
github.com/davecgh/go-spew v1.1.1 // indirect
917
github.com/pmezard/go-difflib v1.0.0 // indirect
18+
github.com/redis/go-redis/v9 v9.7.0
1019
gopkg.in/yaml.v3 v3.0.1 // indirect
1120
)

‎v2/go.sum

+12
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
1+
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
2+
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
3+
github.com/alicebob/miniredis/v2 v2.33.0 h1:uvTF0EDeu9RLnUEG27Db5I68ESoIxTiXbNUiji6lZrA=
4+
github.com/alicebob/miniredis/v2 v2.33.0/go.mod h1:MhP4a3EU7aENRi9aO+tHfTBZicLqQevyi/DJpoj6mi0=
5+
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
6+
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
17
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
28
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
9+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
10+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
311
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
412
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
13+
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
14+
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
515
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
616
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
17+
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
18+
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
719
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
820
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
921
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

0 commit comments

Comments
 (0)
Please sign in to comment.