- Docs
- Background work
- gRPC client
gRPC client
Dial gRPC services with round-robin balancing, retry, tracing, timeouts, and a resilience guard.
Use shared/grpcclient for outbound gRPC calls between Zever services. New(ctx, target, opts...) returns a *grpc.ClientConn with round-robin load balancing, optional per-method retry, W3C trace propagation, metadata, a per-RPC timeout, and an optional resilience.Guard already wired. It is a natural backing for the generated saga SagaCaller (see saga), and container.GRPCClient caches one connection per target.
There is no config entry: grpcclient is a helper, not a battery. Build connections in code or through the container.
Quick start
Section titled “Quick start”package main
import ( "context" "fmt"
"github.com/zenta-dev/zever/shared/grpcclient")
func main() { conn, err := grpcclient.New(context.Background(), "static:///users", grpcclient.WithInsecure(), grpcclient.WithStaticResolver(map[string][]string{ "users": {"127.0.0.1:50051", "127.0.0.2:50051"}, })) if err != nil { panic(err) } defer conn.Close()
fmt.Println("client created")}client createdNew performs no IO, so the connection is established lazily on the first RPC. Call generated clients over it (pb is your generated protobuf package):
client := pb.NewUsersClient(conn)resp, err := client.GetUser(ctx, &pb.GetUserRequest{Id: "u-1"})Choose credentials and a target
Section titled “Choose credentials and a target”Credentials are explicit and fail closed: New returns ErrNoCredentials unless you pass WithInsecure or WithTLS, and ErrInsecureAndTLS when you pass both. WithInsecure is for development and loopback only.
| Target | Resolves through |
|---|---|
dns:///host:port |
gRPC’s built-in DNS resolver. Multiple addresses spread load with round_robin, no sidecar. |
static:///name |
The WithStaticResolver address map. An unknown or empty name fails fast. Registered per connection, never globally. Use for in-process and test setups. |
For production, use TLS. WithTLS(TLSOptions{CertFile, KeyFile, CAFile, ServerName}) loads an optional client certificate pair (both or neither, for mTLS), a CA bundle, and a server-name override. TLS 1.2 is the floor; read or parse failures return errors, and WithTLS never falls back to insecure.
import ( "context" "time"
"google.golang.org/grpc" "google.golang.org/grpc/codes"
"github.com/zenta-dev/zever/shared/grpcclient")
func dial(ctx context.Context) (*grpc.ClientConn, error) { return grpcclient.New(ctx, "dns:///users:443", grpcclient.WithTLS(grpcclient.TLSOptions{CAFile: "/etc/ssl/ca.pem"}), grpcclient.WithMetadata(map[string]string{"x-tenant": "acme"}), grpcclient.WithTimeout(2*time.Second), grpcclient.WithRetryPolicy(grpcclient.RetryPolicy{ MaxAttempts: 5, // capped at DefaultMaxAttempts (5) InitialBackoff: 10 * time.Millisecond, MaxBackoff: 500 * time.Millisecond, BackoffMultiplier: 2, RetryableStatusCodes: []codes.Code{codes.Unavailable}, }), )}Retry failed calls
Section titled “Retry failed calls”WithRetryPolicy merges a per-method policy into the gRPC service config for every method. Invalid policies return an error, never a silent ignore:
MaxAttemptsmust be at least 2 and is capped atDefaultMaxAttempts(5, gRPC’s own client limit).- Backoffs and
BackoffMultipliermust be positive. RetryableStatusCodesmust not be empty. Keep it minimal (typicallyUNAVAILABLE): retrying every failure multiplies load on a struggling server. Code names are mapped to the canonical form for you.
WithServiceConfigJSON overlays raw keys (for example a custom loadBalancingConfig) on the round_robin default.
Resolve the connection from the container
Section titled “Resolve the connection from the container”conn, err := c.GRPCClient("static:///users", grpcclient.WithInsecure())GRPCClient(target, opts...) caches one connection per target string: repeat calls with the same target return the same connection, and later calls with different options keep the first build. The container closes cached connections during Close(ctx); otherwise the caller closes with conn.Close(). See the container.
Know the interceptor order
Section titled “Know the interceptor order”Every connection chains unary and stream interceptors in this order, outermost first:
- Observability span (when
WithObservabilityis set), covering the whole call including guard execution. - W3C trace context injection from
shared/tracepropinto outgoing metadata. WithMetadatakeys, never overwriting keys already in the outgoing context.WithTimeoutdeadline (must be positive, orNewerrors), governing the whole call including guard retries; without it only the caller’s context applies.WithGuard, wrapping the call in theresilience.Guardbreaker, bulkhead, and retry composition.
For streams, the guard wraps stream establishment only; messages after establishment are not guarded.
Production checklist
Section titled “Production checklist”- Use
WithTLSwith a CA bundle, neverWithInsecure. - Pair
round_robinwith theredisresilience adapter when replicas must share one circuit per downstream dependency. - Metadata values can carry tenant or user context. Never log raw metadata maps, and use
config.RedactedServicesfor config output.
Troubleshooting
Section titled “Troubleshooting”ErrNoCredentials
Neither WithInsecure nor WithTLS was passed. Pick one; credentials are explicit.
ErrInsecureAndTLS
Both options were passed; they are mutually exclusive. Remove one.
static resolver: unknown name
The target name is not in the WithStaticResolver map, so the channel fails fast. Add the name and its addresses.
Calls hang past the dependency’s deadline
No WithTimeout, so only the caller’s context bounds the call. Add it; it bounds the whole call including guard retries.
Retries never trigger
RetryableStatusCodes does not list the codes to retry. List them explicitly, for example codes.Unavailable.