Criteri di nuovo tentativo nelle librerie client di C++

In questa pagina viene descritto il modello di nuovo tentativo utilizzato dalle librerie client di C++.

Le librerie client emettono RPC (Remote Procedure Calls) per tuo conto. Queste RPC possono avere esito negativo a causa di errori temporanei. Possono essere applicati limiti di frequenza e i limiti di frequenza dei server, i bilanciatori del carico chiudono le connessioni sovraccaricate o inattive. Questi sono solo alcuni esempi di errori temporanei.

Le librerie potrebbero restituire questi errori all'applicazione. Tuttavia, molti di questi errori sono facili da gestire nella libreria, il che rende più semplice il codice dell'applicazione.

Errori e operazioni ripetibili

Solo gli errori temporanei sono irreversibili. Ad esempio, kUnavailable indica che il client non è riuscito a connettersi o che ha perso la connessione a un servizio mentre era in corso una richiesta. Si tratta quasi sempre di una condizione temporanea, anche se il ripristino può richiedere molto tempo. Questi errori sono sempre ripetibili (supponendo che l'operazione sia sicura). Nel contratto, gli errori kPermissionDenied richiedono un intervento aggiuntivo (di solito da parte di una persona) per essere risolti. Questi errori non sono considerati "temporanei" o almeno non temporanei nei tempi considerati dai loop di nuovi tentativi nella libreria client.

Analogamente, non è sicuro ritentare alcune operazioni, indipendentemente dalla natura dell'errore. Sono incluse le operazioni che apportano modifiche incrementali. Ad esempio, non è sicuro ritentare un'operazione per rimuovere la "versione più recente di X", in cui potrebbero esserci più versioni di una risorsa denominata "X". Questo perché il chiamante probabilmente intendeva rimuovere una singola versione e ritentare la richiesta può comportare la rimozione di tutte le versioni.

Configura i loop di nuovi tentativi

Le librerie client accettano tre diversi parametri di configurazione per controllare i loop di nuovo tentativo:

  • *IdempotencyPolicy determina se una determinata richiesta è idempotente. Vengono tentate solo di nuovo le richieste di questo tipo.
  • *RetryPolicy determina (a) se un errore deve essere considerato un errore temporaneo e (b) per quanto tempo (o quante volte) la libreria client tenta di nuovo una richiesta.
  • *BackoffPolicy determina il tempo di attesa della libreria client prima di riinviare la richiesta.

Criterio predefinito sull'idempotenza

In generale, un'operazione è idempotente se, se richiami la funzione più volte correttamente, il sistema rimane nello stesso stato in cui è riuscita a chiamare la funzione una volta sola. Solo le operazioni idempotenti possono essere riprovate. Tra gli esempi di operazioni idempotenti figurano, a titolo esemplificativo, tutte le operazioni di sola lettura e le operazioni che possono avere esito positivo una sola volta.

Per impostazione predefinita, la libreria client considera come idempotenti solo le RPC implementate tramite i verbi GET o PUT. Questa formula potrebbe essere troppo conservativa, in quanto in alcuni servizi anche alcune richieste POST sono idempotenti. Puoi sempre eseguire l'override del criterio di idempotenza predefinito per soddisfare meglio le tue esigenze.

Alcune operazioni sono idempotenti solo se includono precondizioni. Ad esempio, "rimuovi la versione più recente se l'ultima versione è Y" è idempotente, perché può avere esito positivo solo una volta.

Di tanto in tanto, le librerie client ricevono miglioramenti per trattare più operazioni come idempotenti. Consideriamo questi miglioramenti correzioni di bug e, di conseguenza, permanenti anche se modificano il comportamento della libreria client.

Tieni presente che, anche se può essere sicuro ritentare un'operazione, questo non significa che l'operazione produca lo stesso risultato al secondo tentativo rispetto al primo tentativo riuscito. Ad esempio, potrebbe essere sicuro riprovare a creare una risorsa identificata in modo univoco, poiché il secondo tentativo e quelli successivi hanno esito negativo e il sistema rimane nello stesso stato. Tuttavia, il client potrebbe ricevere un errore di tipo "già esistente" dopo ogni nuovo tentativo.

Criterio predefinito sui nuovi tentativi

Seguendo le linee guida descritte in aip/194, la maggior parte delle librerie client di C++ riprova solo UNAVAILABLE per gli errori gRPC. Questi sono mappati a StatusCode::kUnavailable. Il criterio predefinito prevede di riprovare le richieste per 30 minuti.

Tieni presente che gli errori kUnavailable non indicano che il server non ha ricevuto la richiesta. Questo codice di errore viene utilizzato quando la richiesta non può essere inviata, ma viene utilizzato anche se la richiesta viene inviata e ricevuta dal servizio e la connessione si interrompe prima che il client riceva la risposta. Inoltre, se riesci a determinare se la richiesta è stata ricevuta correttamente, potresti risolvere il problema dei due generali, un risultato impossibilitato nei sistemi distribuiti.

Pertanto, non è sicuro riprovare tutte le operazioni che non vanno a buon fine con kUnavailable. Anche l'idempotenza dell'operazione è importante.

Criterio di backoff predefinito

Per impostazione predefinita, la maggior parte delle librerie utilizza una strategia di backoff esponenziale troncato, con tremolio. Il backoff iniziale è di 1 secondo, il backoff massimo di 5 minuti e il backoff raddoppia dopo ogni tentativo.

Modifica i criteri predefiniti per nuovi tentativi e backoff

Ogni libreria definisce uno struct *Option per configurare questi criteri. Puoi fornire queste opzioni quando crei la classe *Client o anche su ogni richiesta.

Ad esempio, questo mostra come modificare i criteri di nuovo e backoff per 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";
}

Consulta la documentazione di ogni libreria per trovare i nomi e gli esempi specifici per quella libreria.

Passaggi successivi