Repetir políticas nas bibliotecas de cliente do C++

Nesta página, descrevemos o modelo de repetição usado pelas bibliotecas de cliente C++.

As bibliotecas de cliente emitem Chamadas de Procedimento Remoto (RPCs, na sigla em inglês) em seu nome. Essas RPCs podem falhar devido a erros transitórios. Os servidores são reiniciados, os balanceadores de carga fecham conexões sobrecarregadas ou inativas e os limites de taxa podem entrar em vigor. Esses são apenas alguns exemplos de falhas temporárias.

As bibliotecas podem retornar esses erros ao aplicativo. No entanto, muitos desses erros são fáceis de processar na biblioteca, o que simplifica o código do aplicativo.

Erros que podem ser repetidos e operações que podem ser repetidas

Apenas erros transitórios podem ser tentados novamente. Por exemplo, kUnavailable indica que o cliente não conseguiu se conectar ou perdeu a conexão com um serviço enquanto uma solicitação estava em andamento. Essa é quase sempre uma condição temporária, embora possa levar muito tempo para se recuperar. Esses erros são sempre possíveis de repetir, supondo que a operação em si seja segura para novas tentativas. No contrato, os erros kPermissionDenied exigem mais intervenção (geralmente por um ser humano) para serem resolvidos. Esses erros não são considerados "temporários" ou, pelo menos, não são transitórios nas escalas de tempo consideradas pelos loops de repetição na biblioteca de cliente.

Da mesma forma, algumas operações não são seguras para novas tentativas, independentemente da natureza do erro. Isso inclui todas as operações que fazem alterações incrementais. Por exemplo, não é seguro repetir uma operação para remover "a versão mais recente de X", em que pode haver várias versões de um recurso chamado "X". Isso ocorre porque o autor da chamada provavelmente pretendia remover uma única versão, e tentar fazer essa solicitação novamente pode resultar na remoção de todas as versões.

Configurar loops de repetição

As bibliotecas de cliente aceitam três parâmetros de configuração diferentes para controlar os loops de repetição:

  • O *IdempotencyPolicy determina se uma solicitação específica é idempotente. Somente essas solicitações são repetidas.
  • O *RetryPolicy determina (a) se um erro precisa ser considerado uma falha temporária e (b) por quanto tempo (ou quantas vezes) a biblioteca de cliente repete uma solicitação.
  • O *BackoffPolicy determina quanto tempo a biblioteca de cliente aguarda antes de emitir novamente a solicitação.

Política de idempotência padrão

Em geral, uma operação é idempotente quando chamada com sucesso a função várias vezes deixa o sistema no mesmo estado de que a função é chamada uma única vez. Apenas operações idempotentes são seguras para novas tentativas. Exemplos de operações idempotentes incluem, sem limitação, todas as operações somente leitura e operações que só podem ser bem-sucedidas uma vez.

Por padrão, a biblioteca de cliente trata apenas as RPCs implementadas com os verbos GET ou PUT como idempotentes. Isso pode ser muito conservador. Em alguns serviços, mesmo algumas solicitações POST são idempotentes. A qualquer momento, é possível substituir a política de idempotência padrão para atender melhor às suas necessidades.

Algumas operações são idempotentes apenas quando incluem condições prévias. Por exemplo, "remover a versão mais recente se a versão mais recente for Y" é idempotente, já que só pode ser bem-sucedida uma vez.

De tempos em tempos, as bibliotecas de cliente recebem melhorias para tratar mais operações como idempotentes. Consideramos essas melhorias como correções de bugs e, portanto, não interruptivas, mesmo que mudem o comportamento da biblioteca de cliente.

Embora possa ser seguro repetir uma operação, isso não significa que ela produz o mesmo resultado na segunda tentativa em comparação com a primeira tentativa bem-sucedida. Por exemplo, criar um recurso identificado de maneira exclusiva pode ser seguro para novas tentativas, já que a segunda e as tentativas sucessivas falham e deixam o sistema no mesmo estado. No entanto, o cliente pode receber um erro "já existe" nas tentativas de repetição.

Política padrão de nova tentativa

Seguindo as diretrizes descritas em aip/194, a maioria das bibliotecas de cliente C++ só repete os erros gRPC UNAVAILABLE. Eles estão mapeados para StatusCode::kUnavailable. A política padrão é repetir as solicitações por 30 minutos.

Observe que os erros kUnavailable não indicam que o servidor não conseguiu receber a solicitação. Esse código de erro é usado quando a solicitação não pode ser enviada, mas também é usado se a solicitação for enviada com êxito, recebida pelo serviço e a conexão for perdida antes que a resposta seja recebida pelo cliente. Além disso, se você pudesse determinar se a solicitação foi recebida com êxito, poderá resolver o problema dos dois gerais, um conhecido resultado de impossibilidade em sistemas distribuídos.

Portanto, não é seguro repetir todas as operações que falham com kUnavailable. A idempotência da operação também é importante.

Política de espera padrão

Por padrão, a maioria das bibliotecas usa uma estratégia de espera exponencial truncada com instabilidade. A espera inicial é de um segundo, a espera máxima é de cinco minutos e ela dobra após cada nova tentativa.

Alterar políticas padrão de nova tentativa e espera

Cada biblioteca define um struct *Option para configurar essas políticas. É possível fornecer essas opções ao criar a classe *Client ou até mesmo em cada solicitação.

Por exemplo, isto mostra como alterar as políticas de nova tentativa e espera para um cliente do 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";
}

Consulte a documentação de cada biblioteca para ver os nomes e exemplos específicos delas.

Próximas etapas