Faraday: Distinguir TimeoutErrors para tempos limites de abertura e leitura

Criado em 9 ago. 2017  ·  32Comentários  ·  Fonte: lostisland/faraday

Em faraday / adapter / rack.rb, TimeoutError é gerado para tempos limites de abertura e leitura:

timeout  = env[:request][:timeout] || env[:request][:open_timeout]
response = if timeout
  Timer.timeout(timeout, Faraday::Error::TimeoutError) { execute_request(env, rack_env) }
else ... end

De acordo com https://stackoverflow.com/questions/10322283/what-is-timeout-and-open-timeout-in-faraday , open_timeout é para a conexão tcp e o tempo limite é para a resposta lida.

Seria bom ter tipos de exceção separados para esses tempos limite. Então, poderíamos determinar se devemos ou não repetir a solicitação. Adicionar algo como Faraday :: Error :: OpenTimeoutError e Faraday :: Error :: ResponseTimeoutError e usá-los aqui faz sentido?

feature help wanted

Comentários muito úteis

Olá @coberlin , acredito que esta seja uma boa adição, só estou com medo da compatibilidade com versões anteriores.
No entanto, uma possível solução para isso pode ser ter OpenTimeoutError e ResponseTimeoutError para herdar de TimeoutError , de modo que rescue s existentes continuem funcionando conforme o esperado.
Definitivamente vale a pena alguns testes 😃

Todos 32 comentários

Olá @coberlin , acredito que esta seja uma boa adição, só estou com medo da compatibilidade com versões anteriores.
No entanto, uma possível solução para isso pode ser ter OpenTimeoutError e ResponseTimeoutError para herdar de TimeoutError , de modo que rescue s existentes continuem funcionando conforme o esperado.
Definitivamente vale a pena alguns testes 😃

O rack_adapter pode ser o local errado para este recurso. Acho que os aplicativos Rack não fazem necessariamente distinção entre tempos limite de abertura e leitura. Talvez esse recurso funcione no adaptador HTTPClient ou em outros adaptadores? Do adaptador / httpclient.rb:

    @app.call env
  rescue ::HTTPClient::TimeoutError, Errno::ETIMEDOUT
    raise Faraday::Error::TimeoutError, $!
  rescue ::HTTPClient::BadResponseError => err
    if err.message.include?('status 407')
      raise Faraday::Error::ConnectionFailed, %{407 "Proxy Authentication Required "}
    else
      raise Faraday::Error::ClientError, $!
    end
  rescue Errno::ECONNREFUSED, IOError, SocketError
    raise Faraday::Error::ConnectionFailed, $!
  rescue => err
    if defined?(OpenSSL) && OpenSSL::SSL::SSLError === err
      raise Faraday::SSLError, err
    else
      raise
    end

:: HTTPClient :: TimeoutError tem 3 subclasses ConnectTimeoutError, ReceiveTimeoutError, SendTimeoutError, consulte, por exemplo, http://www.rubydoc.info/gems/httpclient/2.1.5.2/HTTPClient/TimeoutError

Faraday já tem Faraday :: Error :: ConnectionFailed. Isso é apropriado para ConnectTimeoutError? Faraday :: Error :: TimeoutError pode ser subclassificado em Faraday :: Error :: ReceiveTimeoutError e Faraday :: Error :: SendTimeoutError.

Faraday já tem Faraday :: Error :: ConnectionFailed. Isso é apropriado para ConnectTimeoutError?

Isso faz sentido, mas não seria compatível com versões anteriores. Temos que ter em mente que as pessoas já estão identificando Faraday::Error::TimeoutError em seus aplicativos, portanto, mudar para ConnectionFailed interromperá esses casos.
O que queremos fazer, em vez disso, é definir 2 subclasses para Faraday::Error::TimeoutError cujos nomes devem ser o mais genéricos possível:

  • Faraday :: Error :: OpenTimeoutError
  • Faraday :: Error :: ReadTimeoutError

A próxima etapa é acessar cada adaptador e mapear as exceções do adaptador de acordo. Por exemplo, para o HTTPClient:

  • HTTPClient :: ConnectTimeoutError ==> Faraday :: Error :: OpenTimeoutError
  • HTTPClient :: ReceiveTimeoutError ==> Faraday :: Error :: ReadTimeoutError
  • HTTPClient :: TimeoutError ==> Faraday :: Error :: TimeoutError (isso também irá capturar SendTimeoutError, que não tenho certeza se tem um mapeamento correspondente em Faraday ou uma configuração específica)

Finalmente, os testes devem ser adicionados sempre que possível :)

Ei pessoal.

Conversamos "há pouco" tempo atrás (https://github.com/lostisland/faraday/pull/324).

Estou tentando novamente (https://github.com/mistersourcerer/faraday/tree/718_mrsrcr_timeout-wrapping-2nd-chance), tentarei abrir um novo PR assim que tiver algum progresso.

Olá @mistersourcerer , obrigado pela cutucada, eu não sabia que a discussão estava acontecendo.
Estou um pouco confuso quando vejo o PR fechado, mas a mudança no código, talvez saiba que você mesclou sua mudança de alguma forma no final 😄
Agradecemos sua ajuda neste caso, pois acho que você já está confortável com o teste de Timeout de seu trabalho anterior (embora estejamos falando de cerca de 3 anos atrás!).
Espero que minha explicação sobre OpenTimeoutError e ReadTimeoutError seja clara, mas se não for o caso, por favor me avise.
Não tenha pressa e abra uma solicitação de pull assim que terminar 👍

Olá @iMacTia.

Se bem me lembro, não conseguimos resolver a situação naquela época. Mas não sei exatamente por quê.
O principal problema era escrever um teste que falhou consistentemente entre todos os adaptadores. Então, eu não acho que meu código foi mesclado na época.
Enfim, eu tive uma ideia para isso alguns anos depois, haha, vamos ver no que dá.

No momento, os testes de _EMSynchrony_ estão falhando no Travis, mas não localmente. tentando descobrir isso. Estou pensando até em abrir um PR "inicial", então talvez possamos discutir isso.

E sua explicação é cristalina, parece a maneira perfeita de ir com ela.

Obrigado pelo ótimo trabalho nisso, cara.

Obrigado @mistersourcerer!

RE suas alterações: não tenho certeza do que aconteceu, mas vejo que @mislav finalmente mesclou suas alterações aqui: https://github.com/lostisland/faraday/commit/f73d13ee09814fa68b37efa7bddafa47331948c2

Então, alegre-se, Errno::ETIMEDOUT já está embrulhado em Faraday::Error::TimeoutError na maioria (senão em todos) dos adaptadores 😄

Obrigado por trabalhar neste @mistersourcerer!

Olhando para o seu commit aqui , eu me pergunto se para compatibilidade com versões anteriores, precisamos de 2 novas subclasses: OpenConnectionError < ConnectionError para Net :: HTTP e OpenTimeoutError < TimeoutError para HttpClient?

Parece haver alguma confusão em torno desse problema.
A razão é que uma decisão foi tomada em # 438 para tratar os erros de "tempo limite de abertura" como ConnectionFailed . Essa é sem dúvida a melhor decisão, mas a realidade é que alguém decidiu seguir esse caminho.
Agora, isso não afeta apenas o adaptador de Rack, mas também todos os outros adaptadores, e seu comportamento provavelmente nem mesmo é consistente.
Estou planejando padronizá-los com o mesmo comportamento da v1.0 e manterei esse problema como referência.

Acompanhamento em meu comentário anterior.

Basicamente, estamos gerando um erro Faraday::ConnectionFailed no caso de um tempo limite de abertura, enquanto levantamos um Faraday::TimeoutError para um tempo limite de leitura. Embora adaptadores diferentes estejam se comportando de maneiras diferentes, esse parece ser o comportamento mais comum.
Isso foi decidido cerca de 3 anos atrás, mas aqui estamos discutindo sobre ter um Faraday::TimeoutError para o primeiro caso também (com subclasses adequadas para distinguir entre abrir e fechar).

Por um lado entendo que seria mais próximo da realidade, mas se eu analisar a questão do ponto de vista da implementação, acho difícil justificar essa mudança.
Se eu ligar para um serviço e receber de volta ConnectionFailed , sei que minha chamada não pode ter sido processada. Provavelmente não alcancei o servidor ou não consegui resolver o nome do host ou algo mais aconteceu.
Se eu receber TimeoutError volta, então minha solicitação pode ter sido processada, ou parcialmente processada, e eu posso ter perdido a resposta. Esse é um caso completamente diferente e exige que verifique com o servidor que eu estava chamando o que aconteceu.

Tornar o tempo limite de abertura uma subcategoria de TimeoutError significa pegar uma situação simples (solicitação não processada) em um domínio mais complexo e certamente requer verificações adicionais para decidir o que fazer: foi um tempo limite de abertura ou uma leitura tempo esgotado?

Nós precisamos:

  1. Decida como aumentar o tempo limite de abertura
  2. Padronizar todos os adaptadores para o mesmo comportamento

@coberlin @ erik-escobedo @mislav @mistersourcerer gostaria de ouvir sua opinião após considerar o acima 😄

Usar ConnectionFailed para erros de tempo limite de abertura faz sentido para mim e forneceria o que eu esperava conseguir ao distinguir os erros de tempo limite de abertura dos outros erros de tempo limite. Para consistência do adaptador, isso significaria, por exemplo, que Net::HTTP está ok como está, mas HTTPClient mudaria, com ConnectTimeoutErrors mapeando para ConnectionFailed em vez de TimeoutError.

Tudo bem, uma vez que decidimos padronizar todos os adaptadores para o mesmo comportamento (na v1.0 obviamente, pois isso será incompatível com versões anteriores)

Jogando um caso de uso no ringue:

No trabalho, estamos sofrendo com alguns tempos limite de abertura devido à falha do Nginx + Kubernetes ao rotear para pods suspensos (ou algo assim). De qualquer forma, o NetHTTP costumava lançar erros OpenTimeout e ReadTimeout, e isso era muito útil para nós depurar qual era qual.

Agora que mudamos para Typhoeus , infelizmente temos todos os tempos limites combinados e é difícil para nós dizer se nosso trabalho nos problemas do nginx + kuber foi melhorado ou se agora estamos fazendo mais solicitações para um sistema. De qualquer maneira, o número de tempos limite é quase o mesmo e, sem separá-los, ficamos presos em adivinhações.

Não acho que apenas adicionar Faraday::OpenTimeoutError seja suficiente, devemos ter Faraday::OpenTimeoutError e Faraday::ReadTimeoutError estendendo-se de Faraday::TimeoutError IMO.

@philsturgeon e quanto à outra solução proposta, isso também ajudaria?

Tempo limite de abertura -> Faraday::ConnectionFailed
Tempo limite de leitura -> Faraday::TimeoutError

Esse deve ser o comportamento em todos os adaptadores, mas infelizmente alguns não estão se comportando como esperado (por exemplo, Typhoeus)

Eu sinto que essas são coisas diferentes.

ConnectionFailed parece "Não tenho ideia de como falar com este servidor", como um DNS / IP inválido etc.

OpenTimeout é "Eu sei onde este servidor está, estou apenas esperando que ele faça alguma coisa"

OpenTimeout é "Eu sei onde este servidor está, estou apenas esperando que ele faça alguma coisa"

Discordo disso, prefiro dizer:

Tempo limite de abertura: Estou tentando entrar em contato com o servidor, mas não consigo alcançá-lo (Observação: conexão não estabelecida ou "aberta" ainda).
Tempo limite de leitura: estabeleci uma conexão com o servidor, mas estou esperando que ele faça alguma coisa (lendo a saída).

Um firewall / proxy / load_balancer com defeito são apenas exemplos simples de como você pode obter um tempo limite de abertura, mas em todos esses casos a conexão com o servidor ainda não foi iniciada. Essa é a parte mais importante para mim. "ConnectionFailed" para mim significa simplesmente: não consegui me conectar ao servidor. E se encaixa perfeitamente nesses casos.

Se você ainda acha que um Faraday :: OpenTimeoutError específico deve existir, eu sugiro que herde de ConnectionFailed vez de TimeoutError mas concordo que seria um pouco confuso e não tenho certeza de como ajudaria na prática.
Consulte meu https://github.com/lostisland/faraday/issues/718#issuecomment -343957963 anterior sobre como isso pode realmente ajudar a gerenciar o erro.

Isso faz sentido? Eu gostaria de encontrar uma solução que se adapte a todos

Aceito suas definições mais precisas para tempo limite aberto, mas chego a uma conclusão diferente.

Você considera o tempo limite de abertura como uma falha de conexão, já que a quantidade de tempo que você deseja esperar por essa conexão é considerada parte da conexão. "Falha ao fazer uma conexão em 5s" certamente faz sentido se você explicar assim, mas não é assim que muitas pessoas pensam.

Para muitos, tempo limite aberto apenas significa que ainda não aconteceu. Isso a torna menos definitiva do que a maioria das falhas de conexão, que é "O servidor está fora do ar" ou "Este DNS é lixo".

Suponho que não importa muito, já que falhas de conexão e tempos limite de abertura devem ser tentados novamente, aqui, como um tempo limite de leitura pode ser considerado motivo para recuar?

Eu concordo, podemos argumentar o quanto quisermos sobre a leitura que se possa aplicar a ela, mas a praticidade do meu ponto é o que você disse também: Se você obtiver um tempo limite aberto, significa que pode repetir a solicitação, se receber um Tempo limite de leitura significa que você deve ter MUITO cuidado, pois sua solicitação pode ter sido processada (total ou parcialmente). Coincidentemente, o significado prático de um tempo limite de abertura corresponde ao de uma conexão com falha, portanto, eu o faria herdar de lá.

Hoje as pessoas estão captando ConnectionFailed e TimeoutError exceções e a lógica por trás muito provavelmente está refletindo o que dissemos antes. Se introduzirmos a nova exceção como uma subclasse de ConnectionFailed então as chances são altas de que a maioria (senão todos) dos aplicativos não precise de nenhuma mudança.

Eu entendo (e concordo) de um ponto de vista semântico que um OpenTimeout é apenas outro tipo de Timeout.

Mas ei, e se chamarmos de ConnectionTimedOut vez disso?

Haveria alguma confusão em torno de open_timeout: X sendo o nome da propriedade que diz quanto tempo esperar até lançar ConnectionTimedOut .

Bom ponto 😞

Chamá-lo de ConnectionOpenTimeout ? Ele deixa claro que é um problema de conexão e que é um tempo limite de abertura. Acho que esse nome continua sendo entendido de acordo com o significado de "Falha ao fazer uma conexão em X segundos", embora algumas pessoas ainda possam se perguntar por que o tempo limite não é um tempo limite. 😅

Parece bom para mim 👍!

@iMacTia ei, se você pudesse me dar algumas dicas de por onde começar, eu poderia tentar fazer isso.

Obrigado @philsturgeon , isso seria ótimo! Permitam-me recapitular os principais pontos em torno disso:

  1. Todas as mudanças precisarão ser feitas no branch v1.0 (pois serão incompatíveis com versões anteriores).
  2. O comportamento do gerenciamento de tempo limite é inconsistente entre os adaptadores, portanto, precisamos padronizá-lo.
  3. O comportamento acordado é o seguinte:
  4. Em caso de timeout OPEN, geraremos um erro ConnectionOpenTimeout que herdará de ConnectionFailed .
  5. Em caso de tempo limite de READ, levantaremos TimeoutError .

Eu perdi alguma coisa?

ConnectionOpenTimeout será adicionado às exceções padrão Faraday :: Request :: Repetir manipuladas?

@mjhoy esse é um bom ponto, mas no momento o middleware Retry não tenta novamente em caso de problemas de conexão. Ele tenta novamente a solicitação apenas se a conexão for bem-sucedida, mas houver um tempo limite. Na verdade, não tenho certeza se faz sentido repetir uma solicitação se o serviço que você está chamando não estiver acessível. Você pode preferir obter a exceção de volta e fazer outra coisa nesse caso.

No entanto, Faraday::Request::Retry é configurável, então nada impede você de adicionar ConnectionOpenTimeout ou mesmo ConnectionFailed à lista de exceções que você deseja que ele controle.

Eu gostaria de ver mais algumas "opiniões da comunidade" antes de adicioná-las à lista de exceções padrão

Ocasionalmente, estamos encontrando OpenTimeout erros com um endpoint de API que precisa ser repetido; parece, pela sua lógica acima, que os tempos limite de abertura são seguros para tentar novamente (mais seguro do que um tempo limite de leitura). Também presumimos que, com o middleware de nova tentativa, os tempos limite de abertura e leitura seriam repetidos; a documentação diz: "Por padrão, ele tenta novamente 2 vezes e trata apenas as exceções de tempo limite." As exceções padrão tratadas são Errno::ETIMEDOUT, Timeout::Error, Error::TimeoutError , e Net::OpenTimeout é uma subclasse de Timeout::Error ; não estava muito claro se Faraday os estava tratando de maneira diferente. Então, talvez a documentação deva ser atualizada? Em qualquer caso, sim, configuramos o middleware; Só estou me perguntando se o padrão faz sentido.

@mjhoy Você está certo ao dizer que Timeout::Error inclui Net::OpenTimeout também, portanto, com a implementação atual, parece que o tempo limite de abertura também deve ser tentado novamente. Além disso, Timeout::Error é resgatado e ressuscitado pelo adaptador em circunstâncias normais, portanto, sua presença na lista de exceção pode ser desnecessária ou apenas para segurança extra.

Assim que terminarmos de refatorar as exceções, o Net::OpenTimeout será gerado como uma nova exceção e como eu disse em meu comentário, mudando o comportamento padrão atual.

Ainda acredito que isso não deveria fazer parte dos padrões, mas é definitivamente algo a se considerar ao fazer o trabalho.

Obrigado por levantar esta questão 😄

Ei, desculpe, isso definhou no acúmulo de minhas equipes por um ano e agora nossas prioridades mudaram bastante. Não estarei trabalhando neste assunto, mas boa sorte!

@iMacTia Alguém está trabalhando nessa mudança? Eu não me importo de pegar isso para a v2.0.

Olá @ ragav0102 , obrigado pelo apoio!
Ninguém está trabalhando nisso ainda, pois ainda estamos nos esforçando para lançar a v1.0.

Nós definitivamente apreciaríamos a ajuda, mas não temos um plano ainda para a v2.0, então não posso dizer quando ela será lançada, então suas alterações podem precisar esperar meses antes de serem usadas.

Se você precisar disso em um de seus projetos, provavelmente não é viável.
Se você acabou de ser aprovado e gostaria de contribuir, sugiro que escolha algo agendado para a v1.0, pois será lançado muito antes 😄

Entendi!

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

jeffb-stell picture jeffb-stell  ·  5Comentários

Lewiscowles1986 picture Lewiscowles1986  ·  4Comentários

iMacTia picture iMacTia  ·  3Comentários

aleksb86 picture aleksb86  ·  3Comentários

mattmill30 picture mattmill30  ·  4Comentários