Your resource for web content, online publishing
and the distribution of digital products.
S M T W T F S
 
 
1
 
2
 
3
 
4
 
5
 
6
 
7
 
8
 
9
 
10
 
11
 
12
 
13
 
14
 
15
 
16
 
17
 
18
 
19
 
20
 
21
 
22
 
23
 
24
 
25
 
26
 
27
 
28
 
29
 
30
 
31
 
 
 

gRPC-Secret: Mastering Deadlines, Timeouts, and Custom Contexts

DATE POSTED:August 1, 2024

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
  1. Define Your Custom Structure: Define the custom structure you want to send.
  2. Serialize the Structure: Convert the custom structure into a string or byte array.
  3. Attach to Context: Add the serialized data to the context metadata.
  4. Transmit: Send the gRPC call with the context.
  5. 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.