gRPC Support
Failsafe-go makes it easy to use any policies with gRPC.
Clients
You can create a failsafe UnaryClientInterceptor
for a policy composition to handle errors, limit load, or limit execution time:
interceptor := failsafegrpc.NewUnaryClientInterceptor[SomeResponse](retryPolicy, circuitBreaker)
client, err := grpc.NewClient(target, grpc.WithUnaryInterceptor(interceptor))
// Perform an RPC with retries and circuit breaking
service := somepkg.NewSomeServiceClient(client)
service.DoSomething(ctx, request)
Servers
On the server side, you can use load limiting or time limiting policies to create a failsafe ServerInHandle
:
inTapHandle := failsafegrpc.NewServerInHandle[any](bulkhead, timeout)
server := grpc.NewServer(grpc.InTapHandle(inTapHandle))
You can also create a failsafe UnaryServerInterceptor
:
interceptor := failsafegrpc.NewUnaryServerInterceptor[SomeResponse](retryPolicy, circuitBreaker)
server := grpc.NewServer(target, grpc.UnaryInterceptor(interceptor))
The difference between these two approaches is that a ServerInHandle
handles a request before it has been decoded whereas a UnaryServerInterceptor
allows your policies to handle the contents of a response.
For most load limiting use cases, prefer ServerInHandle
since it does not create additional server side resources for requests that are rejected. For use cases where a policy needs to inspect the response, such as a Fallback
or a CircuitBreaker
, you can use a UnaryServerInterceptor
.
Retrying gRPC Failures
The failsafegrpc
package provides a NewRetryPolicyBuilder
that can build retry policies with built-in detection of retryable gRPC errors, including Unavailable
, DeadlineExceeded
, and ResourceExhausted
:
retryPolicy := failsafegrpc.NewRetryPolicyBuilder().
WithBackoff(time.Second, 30*time.Second).
WithMaxRetries(3).
Build()
Propagating Priorities
When using policies that support execution prioritization, ideally priorities and levels should be propagated from gRPC clients to servers, and on to the server’s handler. On the client, we can propagate priority or level information from a context through an outgoing request by including an interceptor:
interceptor := failsafegrpc.NewUnaryClientInterceptorWithLevel()
client, err := grpc.NewClient(target, grpc.WithUnaryInterceptor(interceptor))
And on the server, we can decode priority or level information from an incoming request, optionally generate a level if one does not exist but a priority does, and propagate it to the handling context:
interceptor := failsafegrpc.NewUnaryServerInterceptorWithLevel(true)
server := grpc.NewServer(target, grpc.UnaryInterceptor(interceptor))
For distributed systems, you typically want to generate a level, if one does not exist, at the edge of your system, and then propagate the same level for all sub-requests.
Fallbacks
When using a Fallback with a UnaryClientInterceptor
, it’s necessary to set your fallback value against the execution’s LastResult()
since a UnaryClientInterceptor has no way to return an alternative result:
fb := fallback.WithFunc(func(exec failsafe.Execution[*SomeResponse]) (*SomeResponse, error) {
exec.LastResult().Msg = "fallback"
return nil, nil
})
A Fallback with a UnaryServerInterceptor
can be used as usual since UnaryServerInterceptor does allow an alternative result to be returned:
fb := fallback.WithResult(&SomeResponse{Msg: "fallback"})
Context Cancellation
When using Failsafe-go’s gRPC support, Context cancellations are automatically propagated to the RPC context. When an execution is canceled for any reason, such as a Timeout, any outstanding RPC’s context is canceled. Similarly, when using a HedgePolicy, any outstanding hedge RPC contexts are canceled once the first successful response is received.