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

En esta página, se describe el modelo de reintento que usan 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 conexiones sobrecargadas o inactivas y los límites de frecuencia pueden aplicarse, y estos son solo algunos ejemplos de fallas transitorias.

Las bibliotecas podrían mostrar estos errores a la aplicación. Sin embargo, muchos de estos errores son fáciles de manejar 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 estaba en curso una solicitud. Casi siempre es una afección transitoria, aunque puede tardar mucho tiempo en recuperarse. Estos errores siempre se pueden reintentar (suponiendo que la operación en sí es segura). En el contrato, los errores kPermissionDenied requieren intervención adicional (por lo general, de una persona) para resolverse. Esos errores no se consideran "Transitorios" o al menos no transitorios en las escalas de tiempo consideradas por los bucles de reintento en la biblioteca cliente.

Del mismo modo, algunas operaciones no son seguras para reintentarlo, independientemente de la naturaleza del error. Esto incluye cualquier operación que realice cambios incrementales. Por ejemplo, no es seguro reintentar una operación para quitar “la última versión de X”, en la que puede haber varias versiones de un recurso llamado “X”. Esto se debe a que es probable que el llamador haya querido quitar una sola versión, y reintentar esa solicitud puede provocar que se quiten todas las versiones.

Configura los bucles de reintento

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

  • *IdempotencyPolicy determina si una solicitud en particular es idempotente. Solo se reintentan esas solicitudes.
  • La *RetryPolicy determina (a) si un error debe considerarse como una falla transitoria y (b) durante cuánto tiempo (o cuántas veces) la biblioteca cliente reintentará una solicitud.
  • La *BackoffPolicy determina cuánto tiempo esperará la biblioteca cliente antes de volver a emitir la solicitud.

Política de idempotencia predeterminada

En general, una operación es idempotente si llamar correctamente a la función varias veces deja el sistema en el mismo estado en el que se realiza una llamada exitosa a la función una sola vez. Solo es seguro reintentar las operaciones idempotentes. Entre los ejemplos de operaciones idempotentes, se incluyen, entre otros, todas las operaciones de solo lectura y las que solo pueden tener éxito 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; 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, "quitar la última versión si la última versión 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 son rotundas, 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 primero. Por ejemplo, es seguro volver a intentar crear un recurso identificado de manera única, ya que el segundo intento y el sucesivo fallan y dejan el sistema en el mismo estado. Sin embargo, el cliente puede recibir un error “ya existe” en los reintentos.

Política de reintentos predeterminada

Si sigues los lineamientos descritos en aip/194, la mayoría de las bibliotecas cliente de C++ solo reintentan los errores UNAVAILABLE de 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 pudo recibir 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 o recibe correctamente la solicitud, y la conexión se pierde antes de que el cliente reciba la respuesta. Además, si pudieras determinar si la solicitud se recibió de forma correcta, podrías resolver el problema de los dos generales, un resultado de imposibilidad conocido en sistemas distribuidos.

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

Política de retirada predeterminada

De forma predeterminada, la mayoría de las bibliotecas usan una estrategia de retirada exponencial truncada, con jitter. 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 un struct *Option para configurar estas políticas. Puedes proporcionar estas opciones cuando creas la clase *Client o incluso en cada solicitud.

Por ejemplo, aquí se muestra cómo cambiar las políticas de reintento y retirada 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 cada biblioteca.

Próximos pasos