This content originally appeared on HackerNoon and was authored by Tatyana
gRPC, an open-source remote procedure call (RPC) framework, enables efficient and scalable communication between services. One crucial aspect of gRPC is the management of deadlines, request timeouts, and the propagation of context, including custom structures.
\ Understanding these mechanisms helps ensure that services respond promptly, resources are not wasted on operations that exceed a reasonable time frame, and custom metadata is effectively transmitted.
Understanding Deadlines and Request Timeouts
Deadlines
A deadline in gRPC specifies the maximum time by which an operation must be completed. If the operation is not completed within this timeframe, it will be automatically terminated. Deadlines are essential for ensuring that system resources are not tied up indefinitely due to unresponsive or slow services.
Request Timeouts
A request timeout is a period that a client is willing to wait for a response from the server. If the server does not respond within this period, the request is aborted. This mechanism protects the client from hanging indefinitely waiting for a response.
Setting Deadlines and Request Timeouts in gRPC
gRPC provides flexible options for setting deadlines and requesting timeouts both on the client and server sides. Here’s how you can do it in Go:
Client-Side Setting Deadlines
\
import (
"context"
"log"
"time"
"google.golang.org/grpc"
pb "path/to/your/protobuf/package"
)
func main() {
conn, err := grpc.Dial("server_address", grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
client := pb.NewYourServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := client.YourMethod(ctx, &pb.YourRequest{})
if err != nil {
log.Fatalf("could not call method: %v", err)
}
log.Printf("Response: %v", resp)
}
Server-Side Handling
On the server side, gRPC allows you to enforce deadlines and handle scenarios where the client-specified deadline is exceeded:
\
import (
"context"
"log"
"net"
"time"
"google.golang.org/grpc"
pb "path/to/your/protobuf/package"
)
type server struct {
pb.UnimplementedYourServiceServer
}
func (s *server) YourMethod(ctx context.Context, req *pb.YourRequest) (*pb.YourResponse, error) {
select {
case <-time.After(10 * time.Second):
return &pb.YourResponse{}, nil
case <-ctx.Done():
return nil, ctx.Err()
}
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterYourServiceServer(s, &server{})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
\
Propagating Custom Structures in Context
To send custom structures via context in gRPC, you need to serialize the data before attaching it to the context and then deserialize it on the receiving end. This involves converting your custom structures into a format that can be transmitted over the network, such as JSON or Protocol Buffers, and then adding this serialized data to the context metadata.
Step-by-Step Process
- Define Your Custom Structure: Define the custom structure you want to send.
- Serialize the Structure: Convert the custom structure into a string or byte array.
- Attach to Context: Add the serialized data to the context metadata.
- Transmit: Send the gRPC call with the context.
- Extract and Deserialize on the Server: Extract the metadata from the context on the server side and deserialize it back into the custom structure.
Step 1: Define Your Custom Structure
\
type CustomStruct struct {
Field1 string
Field2 int
}
\ Step 2: Serialize the Structure
\
import (
"context"
"encoding/json"
"fmt"
"google.golang.org/grpc/metadata"
)
func serializeCustomStruct(customStruct CustomStruct) (string, error) {
data, err := json.Marshal(customStruct)
if err != nil {
return "", err
}
return string(data), nil
}
\ Step 3: Attach to Context
\
func attachCustomStructToContext(ctx context.Context, customStruct CustomStruct) (context.Context, error) {
serializedData, err := serializeCustomStruct(customStruct)
if err != nil {
return nil, err
}
md := metadata.Pairs("custom-struct", serializedData)
ctx = metadata.NewOutgoingContext(ctx, md)
return ctx, nil
}
\ Step 4: Transmit
\
func main() {
conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
client := pb.NewYourServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
customStruct := CustomStruct{Field1: "value1", Field2: 42}
ctx, err = attachCustomStructToContext(ctx, customStruct)
if err != nil {
log.Fatalf("could not attach custom struct to context: %v", err)
}
resp, err := client.YourMethod(ctx, &pb.YourRequest{})
if err != nil {
log.Fatalf("could not call method: %v", err)
}
log.Printf("Response: %v", resp)
}
\ Step 5: Extract and Deserialize on the Server
\
func deserializeCustomStruct(data string) (CustomStruct, error) {
var customStruct CustomStruct
err := json.Unmarshal([]byte(data), &customStruct)
if err != nil {
return CustomStruct{}, err
}
return customStruct, nil
}
func extractCustomStructFromContext(ctx context.Context) (CustomStruct, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return CustomStruct{}, fmt.Errorf("no metadata found in context")
}
serializedData := md["custom-struct"]
if len(serializedData) == 0 {
return CustomStruct{}, fmt.Errorf("no custom struct found in metadata")
}
return deserializeCustomStruct(serializedData[0])
}
func (s *server) YourMethod(ctx context.Context, req *pb.YourRequest) (*pb.YourResponse, error) {
customStruct, err := extractCustomStructFromContext(ctx)
if err != nil {
return nil, err
}
log.Printf("Received custom struct: %+v", customStruct)
select {
case <-time.After(10 * time.Second):
return &pb.YourResponse{}, nil
case <-ctx.Done():
return nil, ctx.Err()
}
}
\
Implementing Middleware for All gRPC Calls
To handle context propagation, including custom structures, consistently across all gRPC calls, you can use interceptors. Interceptors are middleware that process requests and responses, adding functionality like logging, monitoring, and context metadata handling.
Unary and Streaming Interceptors
You need both unary and streaming interceptors to manage different types of RPC calls:
\
- Unary Interceptors: Handle single request-response cycles.
\
- Streaming Interceptors: Handle streams of requests and responses, including client-side streaming, server-side streaming, and bidirectional streaming.
Unary Interceptor Implementation
Client-Side Unary Interceptor:
\
func unaryClientInterceptor(
ctx context.Context,
method string,
req, reply interface{},
cc *grpc.ClientConn,
invoker grpc.UnaryInvoker,
opts ...grpc.CallOption,
) error {
customStruct, ok := ctx.Value("customStruct").(CustomStruct)
if ok {
ctx, err := attachCustomStructToContext(ctx, customStruct)
if err != nil {
return err
}
}
return invoker(ctx, method, req, reply, cc, opts...)
}
\ Server-Side Unary Interceptor:
\
func unaryServerInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
customStruct, err := extractCustomStructFromContext(ctx)
if err != nil {
return nil, err
}
ctx = context.WithValue(ctx, "customStruct", customStruct)
return handler(ctx, req)
}
Streaming Interceptor Implementation
Client-Side Streaming Interceptor:
\
func streamClientInterceptor(
ctx context.Context,
desc *grpc.StreamDesc,
cc *grpc.ClientConn,
method string,
streamer grpc.Streamer,
opts ...grpc.CallOption,
) (grpc.ClientStream, error) {
customStruct, ok := ctx.Value("customStruct").(CustomStruct)
if ok {
ctx, err := attachCustomStructToContext(ctx, customStruct)
if err != nil {
return nil, err
}
}
return
\ Server-Side Streaming Interceptor:
\
import (
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
// StreamServerInterceptor handles server-side streaming
func streamServerInterceptor(
srv interface{},
ss grpc.ServerStream,
info *grpc.StreamServerInfo,
handler grpc.StreamHandler,
) error {
ctx := ss.Context()
customStruct, err := extractCustomStructFromContext(ctx)
if err != nil {
return err
}
// Add custom struct to context for server handling
newCtx := context.WithValue(ctx, "customStruct", customStruct)
wrapped := grpc_middleware.WrapServerStream(ss)
wrapped.WrappedContext = newCtx
// Handle the request
return handler(srv, wrapped)
}
// Example of using the interceptor in a gRPC server setup
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// Register the interceptors
server := grpc.NewServer(
grpc.UnaryInterceptor(unaryServerInterceptor),
grpc.StreamInterceptor(streamServerInterceptor),
)
// Register your gRPC service implementations here
pb.RegisterYourServiceServer(server, &yourServiceServer{})
if err := server.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
\ By creating and registering unary and streaming interceptors, you can ensure that context propagation, including custom structures, is handled consistently across all gRPC calls. This approach ensures that your custom metadata is properly managed and propagated, allowing you to build robust and flexible gRPC services.
This content originally appeared on HackerNoon and was authored by Tatyana
Tatyana | Sciencx (2024-08-01T23:34:03+00:00) gRPC-Secret: Mastering Deadlines, Timeouts, and Custom Contexts. Retrieved from https://www.scien.cx/2024/08/01/grpc-secret-mastering-deadlines-timeouts-and-custom-contexts/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.