Serviços gRPC confiáveis com prazos finais e cancelamento

Por James Newton-King

Prazos e cancelamento são recursos usados por clientes gRPC para anular chamadas em andamento. Este artigo discute por que os prazos e o cancelamento são importantes e como usá-los em aplicativos gRPC do .NET.

Prazos

Um prazo permite que um cliente gRPC especifique quanto tempo aguardará a conclusão de uma chamada. Quando um prazo é excedido, a chamada é cancelada. A definição de um prazo é importante porque fornece o tempo máximo de execução de uma chamada. Ela impede que serviços funcionando de forma inadequada sejam executados para sempre e o esgotamento dos recursos do servidor. Os prazos são uma ferramenta útil para BLD aplicativos confiáveis e devem ser configurados.

Configuração do prazo:

  • Um prazo é configurado usando CallOptions.Deadline quando uma chamada é feita.
  • Não há valor de prazo padrão. As chamadas gRPC não possuem limite de tempo, a menos que um prazo seja especificado.
  • Um prazo é a hora UTC de quando o prazo é excedido. Por exemplo, DateTime.UtcNow.AddSeconds(5) é um prazo de cinco segundos a partir de agora.
  • Se uma hora passada ou atual for usada, a chamada excederá imediatamente o prazo.
  • O prazo é enviado com a chamada gRPC para o serviço e é rastreado independentemente pelo cliente e pelo serviço. É possível que uma chamada gRPC seja concluída em um computador, mas no momento em que a resposta retornou ao cliente, o prazo foi excedido.

Se um prazo for excedido, o cliente e o serviço terão um comportamento diferente:

  • O cliente anula imediatamente a solicitação HTTP subjacente e gera um erro DeadlineExceeded. O aplicativo cliente pode capturar o erro e exibir uma mensagem de tempo limite para o usuário.
  • No servidor, a solicitação HTTP em execução é anulada e ServerCallContext.CancellationToken é gerado. Embora a solicitação HTTP seja anulada, a chamada gRPC continuará sendo executada no servidor até que o método seja concluído. É importante que o token de cancelamento seja passado para métodos assíncronos para que eles sejam cancelados junto com a chamada. Por exemplo, passar um token de cancelamento para consultas de banco de dados assíncronas e solicitações HTTP. Passar um token de cancelamento permite que a chamada cancelada seja concluída rapidamente no servidor e libere recursos para outras chamadas.

Configure CallOptions.Deadline para definir um prazo para uma chamada gRPC:

var client = new Greet.GreeterClient(channel);

try
{
    var response = await client.SayHelloAsync(
        new HelloRequest { Name = "World" },
        deadline: DateTime.UtcNow.AddSeconds(5));
    
    // Greeting: Hello World
    Console.WriteLine("Greeting: " + response.Message);
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded)
{
    Console.WriteLine("Greeting timeout.");
}

Usando ServerCallContext.CancellationToken em um serviço gRPC:

public override async Task<HelloReply> SayHello(HelloRequest request,
    ServerCallContext context)
{
    var user = await _databaseContext.GetUserAsync(request.Name,
        context.CancellationToken);

    return new HelloReply { Message = "Hello " + user.DisplayName };
}

Prazos e novas tentativas

Quando uma chamada gRPC é configurada com tratamento de falha de repetição e um prazo, o prazo acompanha o tempo em todas as tentativas de uma chamada gRPC. Se o prazo for excedido, uma chamada gRPC anulará imediatamente a solicitação HTTP subjacente, ignorará todas as tentativas restantes e gerará um erro DeadlineExceeded.

Propagando prazos

Quando uma chamada gRPC é feita de um serviço gRPC em execução, o prazo deve ser propagado. Por exemplo:

  1. Chamadas de aplicativo cliente FrontendService.GetUser com um prazo.
  2. FrontendService chama UserService.GetUser. O prazo especificado pelo cliente deve ser especificado com a nova chamada gRPC.
  3. UserService.GetUser recebe o prazo. Ele atingirá o tempo limite corretamente se o prazo do aplicativo cliente for excedido.

O contexto de chamada fornece o prazo com ServerCallContext.Deadline:

public override async Task<UserResponse> GetUser(UserRequest request,
    ServerCallContext context)
{
    var client = new User.UserServiceClient(_channel);
    var response = await client.GetUserAsync(
        new UserRequest { Id = request.Id },
        deadline: context.Deadline);

    return response;
}

A propagação manual de prazos pode ser complicada. O prazo precisa ser passado para cada chamada, e é fácil ignorar acidentalmente. Uma solução automática está disponível com a fábrica de clientes gRPC. Especificando EnableCallContextPropagation:

  • Propaga automaticamente o token de prazo e cancelamento para chamadas filho.
  • Não propaga o prazo se a chamada filho especificar um prazo menor. Por exemplo, um prazo propagado de 10 segundos não será usado se uma chamada filho especificar um novo prazo de 5 segundos usando CallOptions.Deadline. Quando vários prazos estão disponíveis, o menor prazo é usado.
  • É uma excelente maneira de garantir que cenários gRPC complexos e aninhados sempre propaguem o prazo final e o cancelamento.
services
    .AddGrpcClient<User.UserServiceClient>(o =>
    {
        o.Address = new Uri("https://localhost:5001");
    })
    .EnableCallContextPropagation();

Para obter mais informações, consulte Integração de fábrica do cliente gRPC no .NET.

Cancelamento

O cancelamento permite que um cliente gRPC cancele chamadas de execução prolongada que não são mais necessárias. Por exemplo, uma chamada gRPC que transmite atualizações em tempo real é iniciada quando o usuário acessa uma página em um site. O fluxo deve ser cancelado quando o usuário navega para longe da página.

Uma chamada gRPC pode ser cancelada no cliente passando um token de cancelamento com CallOptions.CancellationToken ou chamando Dispose na chamada.

private AsyncServerStreamingCall<HelloReply> _call;

public void StartStream()
{
    _call = client.SayHellos(new HelloRequest { Name = "World" });

    // Read response in background task.
    _ = Task.Run(async () =>
    {
        await foreach (var response in _call.ResponseStream.ReadAllAsync())
        {
            Console.WriteLine("Greeting: " + response.Message);
        }
    });
}

public void StopStream()
{
    _call.Dispose();
}

Os serviços gRPC que podem ser cancelados devem:

  • Passar ServerCallContext.CancellationToken para métodos assíncronos. O cancelamento de métodos assíncronos permite que a chamada no servidor seja concluída rapidamente.
  • Propagar o token de cancelamento para chamadas filho. A propagação do token de cancelamento garante que as chamadas filho sejam canceladas com o pai. Fábrica do cliente gRPC e EnableCallContextPropagation() propaga automaticamente o token de cancelamento.

Recursos adicionais