Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ func DebugLoggingInterceptor() grpc.UnaryServerInterceptor
DebugLoggingInterceptor is the interceptor that logs all request/response from a handler

<a name="DefaultClientInterceptor"></a>
## func [DefaultClientInterceptor](<https://github.com/go-coldbrew/interceptors/blob/main/client.go#L66>)
## func [DefaultClientInterceptor](<https://github.com/go-coldbrew/interceptors/blob/main/client.go#L62>)

```go
func DefaultClientInterceptor(defaultOpts ...any) grpc.UnaryClientInterceptor
Expand All @@ -174,7 +174,7 @@ func DefaultClientInterceptors(defaultOpts ...any) []grpc.UnaryClientInterceptor
DefaultClientInterceptors are the set of default interceptors that should be applied to all client calls

<a name="DefaultClientStreamInterceptor"></a>
## func [DefaultClientStreamInterceptor](<https://github.com/go-coldbrew/interceptors/blob/main/client.go#L71>)
## func [DefaultClientStreamInterceptor](<https://github.com/go-coldbrew/interceptors/blob/main/client.go#L67>)

```go
func DefaultClientStreamInterceptor(defaultOpts ...any) grpc.StreamClientInterceptor
Expand All @@ -183,7 +183,7 @@ func DefaultClientStreamInterceptor(defaultOpts ...any) grpc.StreamClientInterce
DefaultClientStreamInterceptor are the set of default interceptors that should be applied to all stream client calls

<a name="DefaultClientStreamInterceptors"></a>
## func [DefaultClientStreamInterceptors](<https://github.com/go-coldbrew/interceptors/blob/main/client.go#L51>)
## func [DefaultClientStreamInterceptors](<https://github.com/go-coldbrew/interceptors/blob/main/client.go#L47>)

```go
func DefaultClientStreamInterceptors(defaultOpts ...any) []grpc.StreamClientInterceptor
Expand Down Expand Up @@ -245,15 +245,15 @@ func (s *svc) echo(ctx context.Context, req *proto.EchoRequest) (*proto.EchoResp
```

<a name="ExecutorClientInterceptor"></a>
## func [ExecutorClientInterceptor](<https://github.com/go-coldbrew/interceptors/blob/main/client.go#L114>)
## func [ExecutorClientInterceptor](<https://github.com/go-coldbrew/interceptors/blob/main/client.go#L113>)

```go
func ExecutorClientInterceptor(defaultOpts ...grpc.CallOption) grpc.UnaryClientInterceptor
```

ExecutorClientInterceptor returns a unary client interceptor that wraps each RPC in an [Executor](<#Executor>). The executor provides resilience logic such as circuit breaking, retries, or bulkheading.

If no executor is configured \(neither via [SetDefaultExecutor](<#SetDefaultExecutor>) nor per\-call \[WithExecutor\]\), the RPC is invoked directly as a passthrough.
Executor resolution order: per\-call \[WithExecutor\] \> global [SetDefaultExecutor](<#SetDefaultExecutor>). When no executor is configured, the interceptor falls back to [HystrixClientInterceptor](<#HystrixClientInterceptor>) for backward compatibility. When the caller explicitly opts out via \[WithoutExecutor\] or \[WithoutHystrix\], the RPC is invoked directly as a passthrough.

Excluded errors and codes \(set via \[WithExcludedErrors\] / \[WithExcludedCodes\]\) are reported as nil to the executor, preventing them from tripping circuit breakers or retry logic. The original error is still returned to the caller.

Expand All @@ -267,7 +267,7 @@ func FilterMethodsFunc(ctx context.Context, fullMethodName string) bool
FilterMethodsFunc is the default implementation of Filter function

<a name="GRPCClientInterceptor"></a>
## func [GRPCClientInterceptor](<https://github.com/go-coldbrew/interceptors/blob/main/client.go#L98>)
## func [GRPCClientInterceptor](<https://github.com/go-coldbrew/interceptors/blob/main/client.go#L94>)

```go
func GRPCClientInterceptor(_ ...any) grpc.UnaryClientInterceptor
Expand All @@ -285,7 +285,7 @@ func GetDebugLogHeaderName() string
GetDebugLogHeaderName returns the current debug log header name.

<a name="HystrixClientInterceptor"></a>
## func [HystrixClientInterceptor](<https://github.com/go-coldbrew/interceptors/blob/main/client.go#L174>)
## func [HystrixClientInterceptor](<https://github.com/go-coldbrew/interceptors/blob/main/client.go#L220>)

```go
func HystrixClientInterceptor(defaultOpts ...grpc.CallOption) grpc.UnaryClientInterceptor
Expand All @@ -305,7 +305,7 @@ func NRHttpTracer(pattern string, h http.HandlerFunc) (string, http.HandlerFunc)
NRHttpTracer wraps an HTTP handler with New Relic tracing. The configured filterFunc \(see SetFilterFunc\) is consulted on every request: paths it rejects run the underlying handler without starting a New Relic transaction. When pattern is non\-empty, newrelic.WrapHandleFunc is used so its route\-level instrumentation stays intact for non\-filtered paths.

<a name="NewRelicClientInterceptor"></a>
## func [NewRelicClientInterceptor](<https://github.com/go-coldbrew/interceptors/blob/main/client.go#L78>)
## func [NewRelicClientInterceptor](<https://github.com/go-coldbrew/interceptors/blob/main/client.go#L74>)

```go
func NewRelicClientInterceptor() grpc.UnaryClientInterceptor
Expand Down Expand Up @@ -422,13 +422,13 @@ func SetDebugLogHeaderName(name string)
SetDebugLogHeaderName sets the gRPC metadata header name that triggers per\-request log level override. Default is "x\-debug\-log\-level". The header value should be a valid log level \(e.g., "debug"\). Empty names are ignored. Must be called during initialization.

<a name="SetDefaultExecutor"></a>
## func [SetDefaultExecutor](<https://github.com/go-coldbrew/interceptors/blob/main/config.go#L243>)
## func [SetDefaultExecutor](<https://github.com/go-coldbrew/interceptors/blob/main/config.go#L245>)

```go
func SetDefaultExecutor(e Executor)
```

SetDefaultExecutor sets the default [Executor](<#Executor>) used by [ExecutorClientInterceptor](<#ExecutorClientInterceptor>) for all outbound unary RPCs. When set, ExecutorClientInterceptor replaces [HystrixClientInterceptor](<#HystrixClientInterceptor>) in the default client interceptor chain. Must be called during initialization, before any RPCs are made. Not safe for concurrent use.
SetDefaultExecutor sets the default [Executor](<#Executor>) used by [ExecutorClientInterceptor](<#ExecutorClientInterceptor>) for outbound unary RPCs when ColdBrew client interceptors are enabled \(the default\). In that configuration, when no executor is configured \(neither global via SetDefaultExecutor nor per\-call via \[WithExecutor\]\), [ExecutorClientInterceptor](<#ExecutorClientInterceptor>) falls back to [HystrixClientInterceptor](<#HystrixClientInterceptor>) for backward compatibility. Must be called during initialization, before any RPCs are made. Not safe for concurrent use.

<a name="SetDefaultRateLimit"></a>
## func [SetDefaultRateLimit](<https://github.com/go-coldbrew/interceptors/blob/main/config.go#L231>)
Expand Down
112 changes: 63 additions & 49 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,7 @@ func DefaultClientInterceptors(defaultOpts ...any) []grpc.UnaryClientInterceptor
callOptions = append(callOptions, o)
}
}
if defaultConfig.defaultExecutor != nil {
ints = append(ints, ExecutorClientInterceptor(callOptions...))
} else {
ints = append(ints, HystrixClientInterceptor(callOptions...))
}
ints = append(ints, ExecutorClientInterceptor(callOptions...))
ints = append(ints,
grpc_retry.UnaryClientInterceptor(),
NewRelicClientInterceptor(),
Expand Down Expand Up @@ -105,8 +101,11 @@ func GRPCClientInterceptor(_ ...any) grpc.UnaryClientInterceptor {
// RPC in an [Executor]. The executor provides resilience logic such as circuit
// breaking, retries, or bulkheading.
//
// If no executor is configured (neither via [SetDefaultExecutor] nor per-call
// [WithExecutor]), the RPC is invoked directly as a passthrough.
// Executor resolution order: per-call [WithExecutor] > global [SetDefaultExecutor].
// When no executor is configured, the interceptor falls back to
// [HystrixClientInterceptor] for backward compatibility. When the caller
// explicitly opts out via [WithoutExecutor] or [WithoutHystrix], the RPC is
// invoked directly as a passthrough.
//
// Excluded errors and codes (set via [WithExcludedErrors] / [WithExcludedCodes])
// are reported as nil to the executor, preventing them from tripping circuit
Expand All @@ -129,13 +128,18 @@ func ExecutorClientInterceptor(defaultOpts ...grpc.CallOption) grpc.UnaryClientI
}
}

// Resolve executor: per-call > global > nil (passthrough)
// Resolve executor: per-call > global > hystrix fallback
exec := defaultConfig.defaultExecutor
if o.hasExecutor {
exec = o.executor
}
if exec == nil {
return invoker(ctx, method, req, reply, cc, opts...)
if o.hasExecutor {
// Caller explicitly opted out (WithoutExecutor / WithoutHystrix).
return invoker(ctx, method, req, reply, cc, opts...)
}
// No executor configured; fall back to Hystrix for backward compat.
return hystrixFallback(ctx, method, req, reply, cc, invoker, opts, o)
}

var invokerErr error
Expand Down Expand Up @@ -167,61 +171,71 @@ func ExecutorClientInterceptor(defaultOpts ...grpc.CallOption) grpc.UnaryClientI
}
}

// hystrixFallback runs the RPC through the deprecated Hystrix circuit breaker.
// It is the shared Hystrix implementation used both as the executor fallback
// when no executor is configured (neither global nor per-call) and by
// [HystrixClientInterceptor], preserving backward compatibility for services
// that have not migrated.
func hystrixFallback(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts []grpc.CallOption, o clientOptions) error {
if o.disableHystrix {
return invoker(ctx, method, req, reply, cc, opts...)
}
hystrixCmd := o.hystrixName
if hystrixCmd == "" {
hystrixCmd = method
}
newCtx, cancel := context.WithCancel(ctx)
defer cancel()

var invokerErr error
hystrixErr := hystrix.Do(hystrixCmd, func() (err error) {
defer func() {
if r := recover(); r != nil {
err = errors.Wrap(fmt.Errorf("panic inside hystrix method: %s, req: %v, reply: %v", method, req, reply), "Hystrix")
log.Error(ctx, "panic", r, "method", method, "req", req, "reply", reply)
}
}()
defer notifier.NotifyOnPanic(newCtx, method)
invokerErr = invoker(newCtx, method, req, reply, cc, opts...)
for _, excludedErr := range o.excludedErrors {
if stdError.Is(invokerErr, excludedErr) {
return nil
}
}
if st, ok := status.FromError(invokerErr); ok {
if slices.Contains(o.excludedCodes, st.Code()) {
return nil
}
}
return invokerErr
}, nil)
if invokerErr != nil {
return invokerErr
}
return hystrixErr
}
Comment thread
ankurs marked this conversation as resolved.

// Deprecated: HystrixClientInterceptor wraps the unmaintained hystrix-go library.
// Use [SetDefaultExecutor] with a failsafe-go executor instead. Will be removed in v1.
//
// See [ExecutorClientInterceptor] for the replacement.
func HystrixClientInterceptor(defaultOpts ...grpc.CallOption) grpc.UnaryClientInterceptor {
return func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
options := clientOptions{
hystrixName: method,
}
o := clientOptions{}
for _, opt := range defaultOpts {
if opt != nil {
if o, ok := opt.(clientOption); ok {
o.process(&options)
if co, ok := opt.(clientOption); ok {
co.process(&o)
}
}
}
for _, opt := range opts {
if opt != nil {
if o, ok := opt.(clientOption); ok {
o.process(&options)
}
}
}
if options.disableHystrix {
// short circuit if hystrix is disabled
return invoker(ctx, method, req, reply, cc, opts...)
}
newCtx, cancel := context.WithCancel(ctx)
defer cancel()

var invokerErr error
hystrixErr := hystrix.Do(options.hystrixName, func() (err error) {
defer func() {
if r := recover(); r != nil {
err = errors.Wrap(fmt.Errorf("panic inside hystrix method: %s, req: %v, reply: %v", method, req, reply), "Hystrix")
log.Error(ctx, "panic", r, "method", method, "req", req, "reply", reply)
}
}()
defer notifier.NotifyOnPanic(newCtx, method)
invokerErr = invoker(newCtx, method, req, reply, cc, opts...)
for _, excludedErr := range options.excludedErrors {
if stdError.Is(invokerErr, excludedErr) {
return nil
}
}
if st, ok := status.FromError(invokerErr); ok {
if slices.Contains(options.excludedCodes, st.Code()) {
return nil
if co, ok := opt.(clientOption); ok {
co.process(&o)
}
}
return invokerErr
}, nil)
if invokerErr != nil {
return invokerErr
}
return hystrixErr
return hystrixFallback(ctx, method, req, reply, cc, invoker, opts, o)
}
}
6 changes: 4 additions & 2 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,10 @@ func SetDefaultRateLimit(rps float64, burst int) {
}

// SetDefaultExecutor sets the default [Executor] used by [ExecutorClientInterceptor]
// for all outbound unary RPCs. When set, ExecutorClientInterceptor replaces
// [HystrixClientInterceptor] in the default client interceptor chain.
// for outbound unary RPCs when ColdBrew client interceptors are enabled (the
// default). In that configuration, when no executor is configured (neither global
// via SetDefaultExecutor nor per-call via [WithExecutor]), [ExecutorClientInterceptor]
// falls back to [HystrixClientInterceptor] for backward compatibility.
// Must be called during initialization, before any RPCs are made. Not safe for concurrent use.
func SetDefaultExecutor(e Executor) {
defaultConfig.defaultExecutor = e
Expand Down
Loading
Loading