Règles de réessai dans les bibliothèques clientes C++

Cette page décrit le modèle de réessai utilisé par les bibliothèques clientes C++.

Les bibliothèques clientes émettent des RPC (appels de procédure à distance) en votre nom. Ces RPC peuvent échouer en raison d'erreurs temporaires. Les serveurs redémarrent, les équilibreurs de charge ferment les connexions surchargées ou inactives, et les limites de débit peuvent prendre effet. Ce ne sont là que quelques exemples de défaillances temporaires.

Les bibliothèques peuvent renvoyer ces erreurs à l'application. Cependant, la plupart de ces erreurs sont faciles à gérer dans la bibliothèque, ce qui simplifie le code de l'application.

Erreurs et opérations récupérables

Seules les erreurs temporaires peuvent faire l'objet d'une nouvelle tentative. Par exemple, kUnavailable indique que le client n'a pas pu se connecter à un service ou a perdu sa connexion pendant le traitement d'une requête. Il s'agit presque toujours d'une condition temporaire, mais le rétablissement peut prendre du temps. Ces erreurs peuvent toujours faire l'objet d'une nouvelle tentative (en supposant que l'opération elle-même puisse être retentée sans risque). En revanche, les erreurs kPermissionDenied nécessitent une intervention supplémentaire (généralement humaine) pour être résolues. Ces erreurs ne sont pas considérées comme "temporaires", ou du moins pas dans les délais pris en compte par les boucles de nouvelles tentatives de la bibliothèque cliente.

De même, certaines opérations ne peuvent pas être réessayées, quelle que soit la nature de l'erreur. Cela inclut toutes les opérations qui apportent des modifications incrémentales. Par exemple, il n'est pas sûr de réessayer une opération visant à supprimer "la dernière version de X" lorsqu'il peut exister plusieurs versions d'une ressource nommée "X". En effet, l'appelant souhaitait probablement supprimer une seule version, et le fait de réessayer une telle requête peut entraîner la suppression de toutes les versions.

Configurer les boucles de réessai

Les bibliothèques clientes acceptent trois paramètres de configuration différents pour contrôler les boucles de réessai :

  • Le *IdempotencyPolicy détermine si une requête spécifique est idempotente. Seules ces requêtes sont réessayées.
  • *RetryPolicy détermine (a) si une erreur doit être considérée comme un échec temporaire et (b) la durée (ou le nombre de fois) pendant laquelle la bibliothèque cliente relance une requête.
  • Le *BackoffPolicy détermine la durée pendant laquelle la bibliothèque cliente attend avant de renvoyer la requête.

Règle d'idempotence par défaut

En général, une opération est idempotente si l'appel réussi de la fonction plusieurs fois laisse le système dans le même état que l'appel réussi de la fonction une seule fois. Seules les opérations idempotentes peuvent être relancées sans risque. Voici quelques exemples d'opérations idempotentes (liste non exhaustive) : toutes les opérations en lecture seule et les opérations qui ne peuvent réussir qu'une seule fois.

Par défaut, la bibliothèque cliente ne traite comme idempotentes que les RPC implémentées via les verbes GET ou PUT. Cette approche peut être trop conservatrice. Dans certains services, même certaines requêtes POST sont idempotentes. Vous pouvez toujours remplacer la règle d'idempotence par défaut pour mieux répondre à vos besoins.

Certaines opérations ne sont idempotentes que si elles incluent des conditions préalables. Par exemple, la commande "supprimer la dernière version si la dernière version est Y" est idempotente, car elle ne peut réussir qu'une seule fois.

De temps en temps, les bibliothèques clientes sont améliorées pour traiter davantage d'opérations comme idempotentes. Nous considérons ces améliorations comme des corrections de bugs. Elles ne sont donc pas incompatibles, même si elles modifient le comportement de la bibliothèque cliente.

Notez que même s'il peut être sûr de réessayer une opération, cela ne signifie pas que l'opération produira le même résultat lors de la deuxième tentative que lors de la première tentative réussie. Par exemple, la création d'une ressource identifiée de manière unique peut être retentée sans risque, car les deuxième et les tentatives suivantes échouent et laissent le système dans le même état. Toutefois, le client peut recevoir une erreur "existe déjà" lors des tentatives de réessai.

Stratégie de réessai par défaut

Conformément aux consignes décrites dans aip/194, la plupart des bibliothèques clientes C++ ne réessayent que les erreurs gRPC UNAVAILABLE. Elles sont mappées sur StatusCode::kUnavailable. La règle par défaut consiste à réessayer les requêtes pendant 30 minutes.

Notez que les erreurs kUnavailable n'indiquent pas que le serveur n'a pas reçu la requête. Ce code d'erreur est utilisé lorsque la requête ne peut pas être envoyée, mais également si elle est envoyée et reçue par le service, et que la connexion est perdue avant que le client ne reçoive la réponse. De plus, si vous pouviez déterminer si la requête a été reçue avec succès, vous pourriez résoudre le problème des deux généraux, un résultat d'impossibilité bien connu dans les systèmes distribués.

Il n'est donc pas sûr de relancer toutes les opérations qui échouent avec kUnavailable. L'idempotence de l'opération est également importante.

Règle de backoff par défaut

Par défaut, la plupart des bibliothèques utilisent une stratégie d'intervalle exponentiel tronqué entre les tentatives, avec un jitter. L'intervalle d'attente initial est d'une seconde, l'intervalle d'attente maximal est de cinq minutes et l'intervalle d'attente double après chaque nouvelle tentative.

Modifier les règles par défaut de nouvelle tentative et d'attente

Chaque bibliothèque définit une structure *Option pour configurer ces règles. Vous pouvez fournir ces options lorsque vous créez la classe *Client, ou même pour chaque requête.

Par exemple, voici comment modifier les stratégies de nouvelle tentative et de délai avant nouvelle tentative pour un client 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";
}

Consultez la documentation de chaque bibliothèque pour trouver les noms et les exemples spécifiques à cette bibliothèque.

Étapes suivantes