Políticas de reintento en las bibliotecas cliente de C++

En esta página, se describe el modelo de reintentos que utilizan las bibliotecas cliente de C++.

Las bibliotecas cliente emiten RPC (llamadas de procedimiento remoto) en tu nombre. Estas RPC pueden fallar debido a errores transitorios. Los servidores se reinician, los balanceadores de cargas cierran las conexiones inactivas o sobrecargadas, y los límites de frecuencia pueden entrar en vigencia. Estos son solo algunos ejemplos de errores transitorios.

Las bibliotecas podrían devolver estos errores a la aplicación. Sin embargo, muchos de estos errores son fáciles de controlar en la biblioteca, lo que simplifica el código de la aplicación.

Errores y operaciones que se pueden reintentar

Solo se pueden reintentar los errores transitorios. Por ejemplo, kUnavailable indica que el cliente no pudo conectarse o perdió la conexión con un servicio mientras se procesaba una solicitud. Casi siempre es una condición transitoria, aunque puede tardar en recuperarse. Estos errores siempre se pueden reintentar (suponiendo que la operación en sí sea segura para reintentarse). En cambio, los errores de kPermissionDenied requieren intervención adicional (por lo general, de un humano) para resolverse. Estos errores no se consideran "transitorios", o al menos no lo son en los plazos que consideran los bucles de reintento en la biblioteca cliente.

Del mismo modo, algunas operaciones no son seguras para volver a intentarlas, independientemente de la naturaleza del error. Esto incluye cualquier operación que realice cambios incrementales. Por ejemplo, no es seguro volver a intentar una operación para quitar "la versión más reciente de X" cuando puede haber varias versiones de un recurso llamado "X". Esto se debe a que es probable que el llamador haya intentado quitar una sola versión, y volver a intentar esa solicitud puede provocar que se quiten todas las versiones.

Configura bucles de reintentos

Las bibliotecas cliente aceptan tres parámetros de configuración diferentes para controlar los bucles de reintento:

  • El *IdempotencyPolicy determina si una solicitud en particular es idempotente. Solo se reintentan esas solicitudes.
  • El objeto *RetryPolicy determina (a) si un error se debe considerar una falla transitoria y (b) cuánto tiempo (o cuántas veces) la biblioteca cliente reintenta una solicitud.
  • El valor de *BackoffPolicy determina cuánto tiempo espera la biblioteca cliente antes de volver a emitir la solicitud.

Política de idempotencia predeterminada

En general, una operación es idempotente si llamar a la función varias veces de forma correcta deja el sistema en el mismo estado que si se llamara a la función una sola vez de forma correcta. Solo las operaciones idempotentes son seguras para reintentarse. Entre los ejemplos de operaciones idempotentes, se incluyen todas las operaciones de solo lectura y las operaciones que solo pueden completarse una vez.

De forma predeterminada, la biblioteca cliente solo trata como idempotentes las RPC que se implementan a través de los verbos GET o PUT. Esto puede ser demasiado conservador, ya que, en algunos servicios, incluso algunas solicitudes POST son idempotentes. Siempre puedes anular la política de idempotencia predeterminada para que se adapte mejor a tus necesidades.

Algunas operaciones solo son idempotentes si incluyen condiciones previas. Por ejemplo, "quita la versión más reciente si la versión más reciente es Y" es idempotente, ya que solo puede tener éxito una vez.

De vez en cuando, las bibliotecas cliente reciben mejoras para tratar más operaciones como idempotentes. Consideramos que estas mejoras son correcciones de errores y, por lo tanto, no generan interrupciones, incluso si cambian el comportamiento de la biblioteca cliente.

Ten en cuenta que, si bien puede ser seguro reintentar una operación, esto no significa que la operación produzca el mismo resultado en el segundo intento que en el primer intento exitoso. Por ejemplo, crear un recurso identificado de forma única puede ser seguro para volver a intentarlo, ya que el segundo intento y los sucesivos fallan y dejan el sistema en el mismo estado. Sin embargo, es posible que el cliente reciba un error de "ya existe" en los intentos de reintento.

Política de reintentos predeterminada

Según los lineamientos que se describen en aip/194, la mayoría de las bibliotecas cliente de C++ solo reintentan los errores de UNAVAILABLE gRPC. Estos se asignan a StatusCode::kUnavailable. La política predeterminada es reintentar las solicitudes durante 30 minutos.

Ten en cuenta que los errores kUnavailable no indican que el servidor no recibió la solicitud. Este código de error se usa cuando no se puede enviar la solicitud, pero también se usa si la solicitud se envía correctamente, el servicio la recibe y se pierde la conexión antes de que el cliente reciba la respuesta. Además, si pudieras determinar si la solicitud se recibió correctamente, podrías resolver el problema de los dos generales, un resultado de imposibilidad conocido en los sistemas distribuidos.

Por lo tanto, no es seguro reintentar todas las operaciones que fallan con kUnavailable. La idempotencia de la operación también es importante.

Política de retirada predeterminada

De forma predeterminada, la mayoría de las bibliotecas usan una estrategia de retirada exponencial truncada, con fluctuaciones. La retirada inicial es de 1 segundo, la retirada máxima es de 5 minutos y la retirada se duplica después de cada reintento.

Cómo cambiar las políticas predeterminadas de reintento y retirada

Cada biblioteca define una estructura *Option para configurar estas políticas. Puedes proporcionar estas opciones cuando creas la clase *Client o incluso en cada solicitud.

Por ejemplo, se muestra cómo cambiar las políticas de reintentos y de espera exponencial para un cliente de Cloud Pub/Sub:

namespace pubsub = ::google::cloud::pubsub;
using ::google::cloud::future;
using ::google::cloud::Options;
using ::google::cloud::StatusOr;
[](std::string project_id, std::string topic_id) {
  auto topic = pubsub::Topic(std::move(project_id), std::move(topic_id));
  // By default a publisher will retry for 60 seconds, with an initial backoff
  // of 100ms, a maximum backoff of 60 seconds, and the backoff will grow by
  // 30% after each attempt. This changes those defaults.
  auto publisher = pubsub::Publisher(pubsub::MakePublisherConnection(
      std::move(topic),
      Options{}
          .set<pubsub::RetryPolicyOption>(
              pubsub::LimitedTimeRetryPolicy(
                  /*maximum_duration=*/std::chrono::minutes(10))
                  .clone())
          .set<pubsub::BackoffPolicyOption>(
              pubsub::ExponentialBackoffPolicy(
                  /*initial_delay=*/std::chrono::milliseconds(200),
                  /*maximum_delay=*/std::chrono::seconds(45),
                  /*scaling=*/2.0)
                  .clone())));

  std::vector<future<bool>> done;
  for (char const* data : {"1", "2", "3", "go!"}) {
    done.push_back(
        publisher.Publish(pubsub::MessageBuilder().SetData(data).Build())
            .then([](future<StatusOr<std::string>> f) {
              return f.get().ok();
            }));
  }
  publisher.Flush();
  int count = 0;
  for (auto& f : done) {
    if (f.get()) ++count;
  }
  std::cout << count << " messages sent successfully\n";
}

Consulta la documentación de cada biblioteca para encontrar los nombres y ejemplos específicos de esa biblioteca.

Próximos pasos