-
Notifications
You must be signed in to change notification settings - Fork 726
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
client: use interceptor for circuit breaker #8936
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,7 @@ | |
package circuitbreaker | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"strings" | ||
"sync" | ||
|
@@ -62,12 +63,12 @@ | |
} | ||
|
||
// CircuitBreaker is a state machine to prevent sending requests that are likely to fail. | ||
type CircuitBreaker[T any] struct { | ||
type CircuitBreaker struct { | ||
config *Settings | ||
name string | ||
|
||
mutex sync.Mutex | ||
state *State[T] | ||
state *State | ||
|
||
successCounter prometheus.Counter | ||
errorCounter prometheus.Counter | ||
|
@@ -102,8 +103,8 @@ | |
var replacer = strings.NewReplacer(" ", "_", "-", "_") | ||
|
||
// NewCircuitBreaker returns a new CircuitBreaker configured with the given Settings. | ||
func NewCircuitBreaker[T any](name string, st Settings) *CircuitBreaker[T] { | ||
cb := new(CircuitBreaker[T]) | ||
func NewCircuitBreaker(name string, st Settings) *CircuitBreaker { | ||
cb := new(CircuitBreaker) | ||
cb.name = name | ||
cb.config = &st | ||
cb.state = cb.newState(time.Now(), StateClosed) | ||
|
@@ -118,7 +119,7 @@ | |
|
||
// ChangeSettings changes the CircuitBreaker settings. | ||
// The changes will be reflected only in the next evaluation window. | ||
func (cb *CircuitBreaker[T]) ChangeSettings(apply func(config *Settings)) { | ||
func (cb *CircuitBreaker) ChangeSettings(apply func(config *Settings)) { | ||
cb.mutex.Lock() | ||
defer cb.mutex.Unlock() | ||
|
||
|
@@ -129,12 +130,11 @@ | |
// Execute calls the given function if the CircuitBreaker is closed and returns the result of execution. | ||
// Execute returns an error instantly if the CircuitBreaker is open. | ||
// https://github.com/tikv/rfcs/blob/master/text/0115-circuit-breaker.md | ||
func (cb *CircuitBreaker[T]) Execute(call func() (T, Overloading, error)) (T, error) { | ||
func (cb *CircuitBreaker) Execute(call func() (Overloading, error)) error { | ||
state, err := cb.onRequest() | ||
if err != nil { | ||
cb.fastFailCounter.Inc() | ||
var defaultValue T | ||
return defaultValue, err | ||
return err | ||
} | ||
|
||
defer func() { | ||
|
@@ -146,13 +146,13 @@ | |
} | ||
}() | ||
|
||
result, overloaded, err := call() | ||
overloaded, err := call() | ||
cb.emitMetric(overloaded, err) | ||
cb.onResult(state, overloaded) | ||
return result, err | ||
return err | ||
} | ||
|
||
func (cb *CircuitBreaker[T]) onRequest() (*State[T], error) { | ||
func (cb *CircuitBreaker) onRequest() (*State, error) { | ||
cb.mutex.Lock() | ||
defer cb.mutex.Unlock() | ||
|
||
|
@@ -161,7 +161,7 @@ | |
return state, err | ||
} | ||
|
||
func (cb *CircuitBreaker[T]) onResult(state *State[T], overloaded Overloading) { | ||
func (cb *CircuitBreaker) onResult(state *State, overloaded Overloading) { | ||
cb.mutex.Lock() | ||
defer cb.mutex.Unlock() | ||
|
||
|
@@ -170,7 +170,7 @@ | |
state.onResult(overloaded) | ||
} | ||
|
||
func (cb *CircuitBreaker[T]) emitMetric(overloaded Overloading, err error) { | ||
func (cb *CircuitBreaker) emitMetric(overloaded Overloading, err error) { | ||
switch overloaded { | ||
case No: | ||
cb.successCounter.Inc() | ||
|
@@ -185,9 +185,9 @@ | |
} | ||
|
||
// State represents the state of CircuitBreaker. | ||
type State[T any] struct { | ||
type State struct { | ||
stateType StateType | ||
cb *CircuitBreaker[T] | ||
cb *CircuitBreaker | ||
end time.Time | ||
|
||
pendingCount uint32 | ||
|
@@ -196,7 +196,7 @@ | |
} | ||
|
||
// newState creates a new State with the given configuration and reset all success/failure counters. | ||
func (cb *CircuitBreaker[T]) newState(now time.Time, stateType StateType) *State[T] { | ||
func (cb *CircuitBreaker) newState(now time.Time, stateType StateType) *State { | ||
var end time.Time | ||
var pendingCount uint32 | ||
switch stateType { | ||
|
@@ -211,7 +211,7 @@ | |
default: | ||
panic("unknown state") | ||
} | ||
return &State[T]{ | ||
return &State{ | ||
cb: cb, | ||
stateType: stateType, | ||
pendingCount: pendingCount, | ||
|
@@ -227,7 +227,7 @@ | |
// Open state fails all request, it has a fixed duration of `Settings.CoolDownInterval` and always moves to HalfOpen state at the end of the interval. | ||
// HalfOpen state does not have a fixed duration and lasts till `Settings.HalfOpenSuccessCount` are evaluated. | ||
// If any of `Settings.HalfOpenSuccessCount` fails then it moves back to Open state, otherwise it moves to Closed state. | ||
func (s *State[T]) onRequest(cb *CircuitBreaker[T]) (*State[T], error) { | ||
func (s *State) onRequest(cb *CircuitBreaker) (*State, error) { | ||
var now = time.Now() | ||
switch s.stateType { | ||
case StateClosed: | ||
|
@@ -299,7 +299,7 @@ | |
} | ||
} | ||
|
||
func (s *State[T]) onResult(overloaded Overloading) { | ||
func (s *State) onResult(overloaded Overloading) { | ||
switch overloaded { | ||
case No: | ||
s.successCount++ | ||
|
@@ -309,3 +309,25 @@ | |
panic("unknown state") | ||
} | ||
} | ||
|
||
// Define context key type | ||
type cbCtxKey struct{} | ||
|
||
// Key used to store circuit breaker | ||
var CircuitBreakerKey = cbCtxKey{} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you foresee the need have multiple circuit breakers per context for different operations? While this provide a lot flexibility, asking caller to provide the target circuit breaker for each operation is a bit cumbersome and easy to miss on each call path. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's just another way to do it. If it's complicated, I'm ok to keep the status quo. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. pros:
cons:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I really like the approach with interceptor and context as it is a way more elegant. The only concern that it would be easy to miss to pass a circuit break into a context when a new invocation is added at the client layer. Do your think that over time we can enforce presence of the circuit breaker in context for certain calls like get region? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It may not be very convenient for the caller to switch during configuration changes.🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we can get it directly from the SystemVar? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need switch it for every client. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if we use a global variable? |
||
|
||
// FromContext retrieves the circuit breaker from the context | ||
func FromContext(ctx context.Context) *CircuitBreaker { | ||
if ctx == nil { | ||
return nil | ||
} | ||
if cb, ok := ctx.Value(CircuitBreakerKey).(*CircuitBreaker); ok { | ||
return cb | ||
} | ||
return nil | ||
} | ||
|
||
// WithCircuitBreaker stores the circuit breaker into a new context | ||
func WithCircuitBreaker(ctx context.Context, cb *CircuitBreaker) context.Context { | ||
return context.WithValue(ctx, CircuitBreakerKey, cb) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rleungx how do you plan to update circuit breaker settings? Would it be defined at the caller layer (tidb) and passed through the context, so we won't need to plumb ChangeSettings at the client level at all?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the circuit breaker can be defined in the caller layer. Once TiDB variable is changed, it can update the circuit breaker in the caller layer and we don't need to change the client. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure, I just confirming that I've read the PR intent correctly.