Stratégies de nouvelle tentative dans les bibliothèques clientes C++

Cette page décrit le modèle de nouvelle tentative 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 fermant des connexions surchargées ou inactives et les limites de débit peuvent prendre effet. Ce ne sont là que quelques exemples d'échecs temporaires.

Les bibliothèques pourraient renvoyer ces erreurs à l'application. Cependant, bon nombre 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 être relancées. Par exemple, kUnavailable indique que le client n'a pas pu se connecter à un service ou a perdu sa connexion à un service alors qu'une requête était en cours. Il s'agit presque toujours d'un état temporaire, même si sa récupération peut prendre beaucoup de temps. Ces erreurs peuvent toujours être relancées (en supposant que l'opération elle-même puisse être relancée sans risque). Dans le contrat, les erreurs kPermissionDenied nécessitent une intervention supplémentaire (généralement par une personne) pour être résolues. De telles erreurs ne sont pas considérées comme "temporaires", ou du moins pas temporaires dans les délais perçus par les boucles de nouvelle tentative de la bibliothèque cliente.

De même, il n'est pas possible de relancer certaines opérations, quelle que soit la nature de l'erreur. Cela inclut toutes les opérations qui apportent des modifications incrémentielles. Par exemple, il n'est pas prudent de relancer une opération pour supprimer "la dernière version de X" lorsqu'il peut y avoir plusieurs versions d'une ressource nommée "X". En effet, l'appelant a probablement eu l'intention de supprimer une seule version, et toute nouvelle tentative d'une telle requête peut entraîner la suppression de toutes les versions.

Configurer des boucles de nouvelle tentative

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

  • *IdempotencyPolicy détermine si une requête particulière est idempotente. Seules ces requêtes font l'objet d'une nouvelle tentative.
  • Le *RetryPolicy détermine (a) si une erreur doit être considérée comme une défaillance temporaire et (b) le temps (ou le nombre de fois) pendant lequel la bibliothèque cliente effectue de nouvelles tentatives d'exécution d'une requête.
  • *BackoffPolicy détermine le délai d'attente de la bibliothèque cliente avant d'émettre la requête.

Règle d'idempotence par défaut

En général, une opération est idempotente si l'appel réussi à la fonction laisse le système dans le même état qu'un appel réussi de la fonction. Seules les opérations idempotentes peuvent faire l'objet de nouvelles tentatives. 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 que les RPC implémentés via les verbes GET ou PUT comme idempotents. Cette valeur 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, "supprimer la dernière version si la dernière version est Y" est idempotent, car elle ne peut réussir qu'une seule fois.

De temps en temps, les bibliothèques clientes reçoivent des améliorations pour traiter davantage d'opérations comme idempotentes. Nous considérons ces améliorations comme des corrections de bugs et donc non destructives, même si elles modifient le comportement de la bibliothèque cliente.

Bien qu'il soit possible de relancer une opération sans risque, cela ne signifie pas que l'opération produit le même résultat à la deuxième tentative qu'à la première tentative réussie. Par exemple, vous pouvez effectuer une nouvelle tentative sans risque pour créer une ressource identifiée de manière unique, car les deuxièmes tentatives successives échouent et laissent le système dans le même état. Toutefois, le client peut recevoir le message d'erreur "existe déjà" lors des nouvelles tentatives.

Règle de nouvelle tentative 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. Celles-ci sont mappées avec StatusCode::kUnavailable. La règle par défaut consiste à relancer 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 il l'est également si la requête est correctement envoyée, reçue par le service et si 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 bien été reçue, 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 prudent de relancer toutes les opérations qui échouent avec kUnavailable. L'idempotence de l'opération est également importante.

Règle d'intervalle entre les tentatives par défaut

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

Modifier les règles de nouvelle tentative et d'intervalle entre les tentatives par défaut

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.

L'exemple suivant montre comment modifier les stratégies de nouvelle tentative et d'intervalle entre les tentatives 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 ses noms et exemples spécifiques.

Étapes suivantes