Надежные службы gRPC с крайними сроками и отменой

Автор: Джеймс Ньютон-Кинг (James Newton-King)

Крайние сроки и отмена — это функции, используемые клиентами gRPC для прерывания выполняющихся вызовов. В этой статье описывается, в чем заключается важность крайних сроков и отмены и как использовать их в приложениях .NET gRPC.

Крайние сроки

Крайний срок позволяет клиенту gRPC указать, как долго он будет ожидать завершения вызова. При наступлении крайнего срока вызов отменяется. Задавать крайний срок важно по той причине, что он ограничивает длительность выполнения вызова. Это позволяет предотвратить бесконечное выполнение служб и исчерпание ресурсов сервера. Крайние сроки — это полезное средство для повышения надежности приложений, поэтому ими не следует пренебрегать.

Настройка крайнего срока:

  • Крайний срок настраивается с помощью CallOptions.Deadline при выполнении вызова.
  • Значение по умолчанию отсутствует. Если крайний срок не указан, время выполнения вызовов gRPC не ограничено.
  • Крайний срок — это время в формате UTC. Например, DateTime.UtcNow.AddSeconds(5) означает, что крайний срок наступает через 5 секунд от текущего момента.
  • Если указано время в прошлом или текущий момент, крайний срок наступает немедленно.
  • Крайний срок передается службе вместе с вызовом gRPC и отслеживается независимо клиентом и службой. Возможна ситуация, когда выполнение вызова gRPC завершается на одном компьютере, но к тому времени, когда ответ возвращается клиенту, крайний срок уже истекает.

При наступлении крайнего срока клиент и служба ведут себя по-разному.

  • Клиент немедленно прерывает базовый HTTP-запрос и выдает ошибку DeadlineExceeded. Клиентское приложение может перехватить ошибку и вывести сообщение об истечении времени ожидания пользователю.
  • На сервере выполнение HTTP-запроса прерывается, и вызывается ServerCallContext.CancellationToken. Несмотря на то, что HTTP-запрос прерывается, вызов gRPC продолжает выполняться на сервере до тех пор, пока метод не завершит выполнение. Важно передавать токен отмены асинхронным методам, чтобы они отменялись вместе с вызовом. Например, необходимо передавать токен отмены в асинхронные запросы к базе данных и HTTP-запросы. Передача токена отмены позволяет быстро завершить отмененный вызов на сервере и высвободить ресурсы для других вызовов.

Чтобы задать крайний срок для вызова gRPC, настройте CallOptions.Deadline:

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.");
}

Используйте ServerCallContext.CancellationToken в службе 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 };
}

Крайние сроки и повторные попытки

Если для вызова gRPC настроена обработка ошибок с помощью повторных попыток и крайний срок, этот крайний срок будет отслеживать время всех повторных попыток вызова gRPC. При превышении крайнего срока вызов gRPC немедленно прерывает соответствующий HTTP-запрос, пропускает оставшиеся повторные попытки и выдает ошибку DeadlineExceeded.

Распространение крайних сроков

При выполнении вызова gRPC из работающей службы gRPC необходимо распространить крайний срок. Например:

  1. Клиентское приложение вызывает FrontendService.GetUser с крайним сроком.
  2. FrontendService вызывает UserService.GetUser. Крайний срок, заданный клиентом, должен быть указан с новым вызовом gRPC.
  3. UserService.GetUser получает крайний срок. При наступлении крайнего срока время ожидания в клиентском приложении корректно истекает.

В контексте вызова крайний срок указывается с помощью 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;
}

Ручное распространение крайних сроков может быть обременительным. Крайний срок должен быть передан каждому вызову, и легко что-то упустить. Фабрика клиента gRPC позволяет автоматизировать этот процесс. Указание EnableCallContextPropagation:

  • автоматически распространяет крайний срок и токен отмены на дочерние вызовы;
  • Не распространяет крайний срок, если для дочернего вызова установлен меньший срок. Например, распространяемый крайний срок в 10 секунд не используется, если дочерний вызов задает новый крайний срок 5 секунд с использованием CallOptions.Deadline. Когда доступно несколько крайних сроков, используется наименьший.
  • является отличным способом обеспечить распространение крайнего срока и отмены в случае со сложными вложенными вызовами gRPC.
services
    .AddGrpcClient<User.UserServiceClient>(o =>
    {
        o.Address = new Uri("https://localhost:5001");
    })
    .EnableCallContextPropagation();

Дополнительные сведения см. в статье Интеграция фабрики клиента gRPC в .NET.

Отмена

Отмена позволяет клиенту gRPC отменять долго выполняющиеся вызовы, которые больше не нужны. Например, когда пользователь посещает страницу на веб-сайте, выполняется вызов gRPC для потоковой передачи обновлений в режиме реального времени. Потоковая передача должна быть отменена, когда пользователь покидает страницу.

Вызов gRPC можно отменить в клиенте путем передачи токена отмены с помощью CallOptions.CancellationToken или вызова Dispose.

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();
}

Отменяемые службы gRPC должны отвечать указанным ниже требованиям.

  • В асинхронные методы должен передаваться ServerCallContext.CancellationToken. Отмена асинхронных методов позволяет быстро завершить вызов на сервере.
  • Токен отмены должен распространяться на дочерние вызовы. Распространение токена отмены гарантирует, что дочерние вызовы будут отменены вместе с родительскими. Фабрика клиента gRPC и EnableCallContextPropagation() автоматически распространяют токен отмены.

Дополнительные ресурсы