← 中文资源

简体中文版本

gRPC 客户端截止时间与取消传播

为 gRPC 调用设置双层截止时间:父级预算沿 context 逐层收窄,状态码分类超时、取消与不可用三种情况驱动重试决策。

阅读英文原文

示例说明

gRPC 的 deadline 随 context 在调用链传播:客户端设一点二秒、父级只有两秒,实际生效的是更紧的那个;服务端继续把自己的剩余预算传给下游调用——整条链路自动形成漏斗,不会出现外层等两秒内层跑三十秒的错配。状态码是分类依据:超时不可重试、取消源于调用方、不可用可换节点重试。

技术要点

  • deadline 沿 context 传播逐层收窄
  • status.Convert 提取结构化状态码
  • DefaultCallTimeout 兜底无 context 的调用

代码解读

reserveWithDeadline 派生一点二秒截止的 context 传入 Reserve 调用;错误经 status.Convert 转为结构化状态后按码分派——三个分支的消息分别面向超时、取消与不可用场景。连接级 WithDefaultCallTimeout 给漏传 context 的调用兜底。父级两秒预算演示双层截止的收窄关系。

使用建议

截止预算要在入口统一规划。重试仅对 Unavailable 类瞬时码且换节点进行。持续超时是容量信号,配合熔断处理。

代码示例

package main

import (
	"context"
	"fmt"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	pb "example.com/proto/inventory/v1"
)

func reserveWithDeadline(
	ctx context.Context,
	client pb.InventoryClient,
	sku string, qty int32,
) (*pb.ReserveReply, error) {
	ctx, cancel := context.WithTimeout(ctx, 1200*time.Millisecond)
	defer cancel()

	reply, err := client.Reserve(ctx, &pb.ReserveRequest{
		Sku: sku, Quantity: qty,
	})
	if err != nil {
		st := status.Convert(err)
		switch st.Code() {
		case codes.DeadlineExceeded:
			return nil, fmt.Errorf("reserve timed out: %v", st.Message())
		case codes.Canceled:
			return nil, fmt.Errorf("reserve canceled by caller: %v", st.Message())
		case codes.Unavailable:
			return nil, fmt.Errorf("service unavailable, retry later: %v", st.Message())
		default:
			return nil, fmt.Errorf("reserve failed: %v", err)
		}
	}
	return reply, nil
}

func main() {
	conn, err := grpc.NewClient(
		"inventory.example.com:443",
		grpc.WithDefaultCallTimeout(5*time.Second),
	)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer conn.Close()

	client := pb.NewInventoryClient(conn)

	parent, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()

	reply, err := reserveWithDeadline(parent, client, "SKU-1", 3)
	fmt.Println(reply, err)
}