> For the complete documentation index, see [llms.txt](https://consumindo-apis-com-elixir.cafecomelixir.com.br/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://consumindo-apis-com-elixir.cafecomelixir.com.br/problemas-de-api-externa/rate-limite-de-curta-duracao/reexecutando-uma-requisicao.md).

# Reexecutando uma requisição

Alguns limitadores podem levar apenas alguns segundos para liberar, como por exemplo.

* Máximo de 3 requisições por segundo

Isso quer dizer que ao passar o segundo, teremos mais 10 requisições para fazer. Em termos de tempo, faz sentido aguardar o bloqueio. Adicionar esse tempo a requisição não impacta de forma tão negativa quanto receber uma mensagem de erro.&#x20;

Vamos primeiro criar nosso cenário de erro.

<pre class="language-elixir" data-title="test/coffee_shop/integrations/coffee/client_test.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.ClientTest do
  use ExUnit.Case

  alias CoffeeShop.Integrations.Coffee.Client
  alias CoffeeShop.Integrations.Coffee.Response

  describe "all_hot_coffees/0" do
    # ...

    test "recovery of too much requests", %{bypass: bypass} do
      Bypass.expect_once(bypass, "GET", "/coffee/hot", fn conn ->
<strong>        Plug.Conn.resp(conn, 429, "")
</strong>      end)

      opts = [
        base_url: "http://localhost:3000"
      ]

      assert {:ok, %Response{status: status}} = Client.all_hot_coffees(opts)

<strong>      assert status == 200
</strong>    end
  end
end

</code></pre>

A ideia desse teste é conseguir se recuperar de um 429 utilizando a reexecução da requisição, esperando 1 segundos para acabar a penalidade. Obviamente, se rodar esse teste, receberemos um status 429.&#x20;

Para resolver esse problema, iremos utilizar um middleware do tesla, chamado [Tesla.Middleware.Retry](https://hexdocs.pm/tesla/Tesla.Middleware.Retry.html) onde passaremos os seguintes argumentos:

* ***delay*** -> Tempo de espera até próxima tentativa
* ***max\_retries*** -> Máximo de tentativas até retornar um erro
* ***max\_delay*** -> Máximo de tempo de espera
* ***should\_retry*** -> regra condicional para definir se uma requisição deve ou não reexecutar

Vamos lá. Não estamos mais usando a macro do Tesla, isso quer dizer, que não conseguiremos utilizar tiretamente o plug. Isso é bom, pode parecer mais trabalhoso, mas é bom. Para isso, precisamos definir os middlewares diretamente no [*Tesla.Client*](https://hexdocs.pm/tesla/Tesla.Client.html) passando os middlewares, que queremos. Até agora utilizamos o `Tesla.get/1`, passando por parâmetro o caminho completo de nosso *endpoint*. Porém, existe a função `Tesla.get/2`, onde o primeiro argumento é o `Client` e o segundo o *endpoint*. precisamos do client para configurar o middleware, usaremos ele.

<pre class="language-elixir" data-title="lib/integrations/coffee/client.ex" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.Client do
  alias CoffeeShop.Integrations.Coffee.Response

  def all_hot_coffees(opts \\ []) do
    base_url = Keyword.get(opts, :base_url, "https://api.sampleapis.com")

<strong>    middlewares = [
</strong><strong>      {Tesla.Middleware.Retry,
</strong><strong>       delay: 1000,
</strong><strong>       max_retries: 3,
</strong><strong>       max_delay: 2_000,
</strong><strong>       should_retry: fn
</strong><strong>         {:ok, %{status: status}} when status in [429] -> true
</strong><strong>         {:ok, _} -> false
</strong><strong>         {:error, _} -> false
</strong><strong>       end}
</strong><strong>    ]
</strong>
    middlewares
<strong>    |> Tesla.client()
</strong>    |> Tesla.get("#{base_url}/coffee/hot")
    |> Response.build()
  end
end

</code></pre>

Conseguimos adicionar o middleware, mesmo tendo ficado meio cheio a função. Resolveremos isso depois. Agora você pode rodar seu teste.

Algo que devemos notar é a configuração should\_retry. É onde criamos a regra para rodar o *retry*. Ali temos a regra

* `{:ok, _} -> false` # Se nossa resposta for um :ok ele não deve rodar o retry
* `{:error, _} -> false` # Se for um :error não deve rodar (esse erro vem do Tesla, conexão estabelecida por alguma razão, seria um bom ponto para retry, mas não estamos vendo isso agora
* `{:ok, %{status: status}} when status in [429] -> true` # Esse estava no topo, mas coloquei por ultimo aqui para explicar melhor

Pegamos por pattern matching o status vindo do Tesla. Verificamos se esse status está dentro da lista ao fazer `status in [429]` caso esteja, queremos que faça o retry. Esta dentro de uma lista devido a poder colocar mais status ali. Um erro 500 seria legal, assim, alguns erros podem ser sanados sem nem transparecer para o usuário. Porem, estou me atentando apenas ao nosso inimigo 429.

Vamos rodar o teste.

```
mix test test/coffee_shop/integrations/coffee/client_test.exs
```

<pre class="language-elixir"><code class="lang-elixir">> mix test
15:35:22.169 [error] #PID&#x3C;0.285.0> running Bypass.Plug (connection #PID&#x3C;0.283.0>, stream id 2) terminated
Server: localhost:3000 (http)
Request: GET /coffee/hot
** (exit) an exception was raised:
    ** (RuntimeError) route error
        (bypass 2.1.0) lib/bypass/plug.ex:28: Bypass.Plug.call/2
        (plug_cowboy 2.7.1) lib/plug/cowboy/handler.ex:11: Plug.Cowboy.Handler.init/2
        (cowboy 2.12.0) /home/iago-effting/code/study/book/coffee_shop/deps/cowboy/src/cowboy_handler.erl
:37: :cowboy_handler.execute/2
        (cowboy 2.12.0) /home/iago-effting/code/study/book/coffee_shop/deps/cowboy/src/cowboy_stream_h.er
l:306: :cowboy_stream_h.execute/3
        (cowboy 2.12.0) /home/iago-effting/code/study/book/coffee_shop/deps/cowboy/src/cowboy_stream_h.er
l:295: :cowboy_stream_h.request_process/3
        (stdlib 5.2) proc_lib.erl:241: :proc_lib.init_p_do_apply/3


  1) test all_hot_coffees/0 recovery of too much requests (CoffeeShop.Integrations.Coffee.ClientTest)
     test/coffee_shop/integrations/coffee/client_test.exs:31
     match (=) failed
     code:  assert {:ok, %Response{status: status}} = Client.all_hot_coffees(opts)
     left:  {:ok, %CoffeeShop.Integrations.Coffee.Response{status: status}}
     right: {
              :error,
              %CoffeeShop.Integrations.Coffee.Response{
<strong>                status: 500,
</strong>                body: %{},
                error: "Não foi possível se conectar ao serviço de cafés. Tento novamente mais tarde"
              }
            }
     stacktrace:
       test/coffee_shop/integrations/coffee/client_test.exs:40: (test)


Finished in 1.0 seconds (0.00s async, 1.0s sync)
3 tests, 1 failure, 2 excluded




</code></pre>

Recebemos um status 500. Isso foi uma falha em nosso serviço falso. O culpado é a função do `Bypass.expect_once/4` isso acontece por que o *Bypass* esperava apenas uma chamada dessa requisição (expect\_once), mas recebemos mais do que uma, comprovando que nosso *retry* funcionou.

*Bypass* possui a função `Bypass.expect/4` que funciona igual ao `expect_once/4`, mas sem o limitador de ser apenas uma vez. Vamos substituir ela por apenas `expect/4` e o resto continua igual

<pre class="language-elixir" data-title="test/integrations/coffee/client_test.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.ClientTest do
  use ExUnit.Case

  alias CoffeeShop.Integrations.Coffee.Client
  alias CoffeeShop.Integrations.Coffee.Response

  describe "all_hot_coffees/0" do
    # ...
    
    test "too much requests", %{bypass: bypass} do
<strong>      Bypass.expect(bypass, "GET", "/coffee/hot", fn conn ->
</strong>        Plug.Conn.resp(conn, 429, "")
      end)

      opts = [
        base_url: "http://localhost:3000"
      ]

      assert {:ok, %Response{status: status}} = Client.all_hot_coffees(opts)

      assert status == 200
    end
  end
end

</code></pre>

Se rodarmos o teste novamente, iremos perceber duas coisas.

1. O teste ainda falha (o que faz sentido, a resposta das duas requisições é o status 429
2. O teste ficou mais lento.

Vamos resolver a primeira parte. A resposta para as duas requisições em nosso serviço falso é está sendo o 429:&#x20;

```elixir
Plug.Conn.resp(conn, 429, "")
```

Isso quer dizer, nunca receberemos o 200 dessa forma. Precisamos criar um mecanismo que entenda a quantidade de requisições feitas nesse teste. Infelizmente nem o bypass nem o Tesla nos ajudam nessa hora. Não possui uma forma simples de saber, uma vez que o *conn* vindo do *bypass* não se comunica diretamente com a resposta do mesmo. Iremos precisar de algo que rode internamente no *bypass* e que possamos obter o resultado de contagem fora dele. Podemos usar um [Agent](https://hexdocs.pm/elixir/1.12.3/Agent.html).

Agents são uma abstração em torno de um GenServer. GenServer mantem estado e podemos resgatar pelo PID. Usando Agente, não precisamos do PID e sim, apenas da instância. Exatamente o que precisamos. Vamos usar o exemplo que tem na  documentação, é exatamente um contador =D

Criarei ele em `lib/integrations/couter.ex`

{% code title="lib/integrations/couter.ex" lineNumbers="true" %}

```elixir
defmodule CoffeeShop.Integrations.Counter do
  use Agent

  def start_link(initial_value) do
    Agent.start_link(fn -> initial_value end, name: __MODULE__)
  end

  def value do
    Agent.get(__MODULE__, & &1)
  end

  def increment do
    Agent.update(__MODULE__, &(&1 + 1))
  end
end
```

{% endcode %}

Podemos agora usar as funções

* `Counter.start_link/1` para iniciar nosso processo
* `Counter.increment/0` para adicionar 1 ao contador
* `Couter.value/0` para obter o valor total incrementado

Vamos adicionar esse ao nosso teste.

<pre class="language-elixir" data-title="test/integrations/coffee/client_test.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.ClientTest do
  use ExUnit.Case

  alias CoffeeShop.Integrations.Coffee.Client
  alias CoffeeShop.Integrations.Coffee.Response
  alias CoffeeShop.Integrations.Counter

  describe "all_hot_coffees/0" do
    test "too much requests", %{bypass: bypass} do
<strong>      Counter.start_link(0)
</strong>
      Bypass.expect(bypass, "GET", "/coffee/hot", fn conn ->
<strong>        IO.inspect(Counter.value())
</strong>        
<strong>        Counter.increment()
</strong>        Plug.Conn.resp(conn, 429, "")
      end)

      opts = [
        base_url: "http://localhost:3000"
      ]

      assert {:ok, %Response{status: status}} = Client.all_hot_coffees(opts)

      assert status == 200
    end
  end
end

</code></pre>

Os pontos importantes ali são.

* Iniciamos o *Counter* fora da chamada do *Bypass*, bem no inicio do teste.
* Incrementamos toda vez que o *Bypass* identificar uma nova requisição
* Adicionei também um `IO.inspect/1` para vermos o valor aparecer em nosso console.

Podemos rodar o teste agora e veremos o valor sendo incrementado

```
mix test
0
1
2
3


  1) test all_hot_coffees/0 too much requests (CoffeeShop.Integrations.Coffee.ClientTest)
     test/integrations/coffee/client_test.exs:45
     Assertion with == failed
     code:  assert status == 200
     left:  429
     right: 200
     stacktrace:
       test/integrations/coffee/client_test.exs:60: (test)

.....
Finished in 4.7 seconds (0.00s async, 4.7s sync)
1 doctest, 5 tests, 1 failure
```

A contagem ficou de 0, 1, 2 e 3, Isso quer dizer, o 0 foi a primeira requisição e quando ele falhou começou a processar os *retry*, tendo 3 chances para se recuperar, ao chegar no limite máximo ele simplesmente ficou com a resposta da última tentativa. Percebe como nossos *retries* estão funcionando bem? Falta apenas uma logica para mockarmos o retorno de uma desses retries de forma a ser um sucesso.

<pre class="language-elixir" data-title="test/integrations/coffee/client_test.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.ClientTest do
  use ExUnit.Case

  alias CoffeeShop.Integrations.Coffee.Client
  alias CoffeeShop.Integrations.Coffee.Response
  alias CoffeeShop.Integrations.Counter

  describe "all_hot_coffees/0" do
    #...
    test "too much requests", %{bypass: bypass} do
      Counter.start_link(0)
      
      response =
        File.read!("test/coffee_shop/integrations/coffee/fixtures/success.json")

      Bypass.expect(bypass, "GET", "/coffee/hot", fn conn ->
<strong>        case Counter.value() do
</strong><strong>          0 = _first_call ->
</strong><strong>            Counter.increment()
</strong><strong>            Plug.Conn.resp(conn, 429, "Too many requests")
</strong><strong>
</strong><strong>          1 = _first_retry_call ->
</strong><strong>            Plug.Conn.resp(conn, 200, response)
</strong><strong>        end
</strong>      end)

      opts = [
        base_url: "http://localhost:3000"
      ]

      assert {:ok, %Response{status: status}} = Client.all_hot_coffees(opts)

      assert status == 200
    end
  end
end

</code></pre>

O case é utilziado para saber qual resposta devolvemos. Simulamos ali, uma resposta com status 429 e em seguida, com o retry, uma nova resposta com o 200. Isso garante que o retry está funcionando e que temos tratamento para 429.

```
mix test
```

```
> time mix test
Compiling 1 file (.ex)
......
Finished in 1.8 seconds (0.00s async, 1.8s sync)
1 doctest, 5 tests, 0 failures
```

Lindo não?

## O problema do Delay

Temos um pequeno problema no nosso código, para ser especifico, um problema em rodar os testes. Façamos um experimento. Entre no seu cliente e altere o valor do delay e max\_delay para 10\_000:

<pre class="language-elixir" data-title="lib/integrations/coffee/client.ex" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.Client do
  alias CoffeeShop.Integrations.Coffee.Response

  def all_hot_coffees(opts \\ []) do
    base_url = Keyword.get(opts, :base_url, "https://api.sampleapis.com")

    middlewares = [
      {Tesla.Middleware.Retry,
<strong>       delay: 10_000,
</strong>       max_retries: 3,
<strong>       max_delay: 10_000,
</strong>       should_retry: fn
         {:ok, %{status: status}} when status in [429] -> true
         {:ok, _} -> false
         {:error, _} -> true
       end}
    ]

    Tesla.client(middlewares)
    |> Tesla.get("#{base_url}/coffee/hot")
    |> Response.build()
  end
end

</code></pre>

Rode esse teste e veja que o teste irá passar.&#x20;

```
> time mix test
Compiling 1 file (.ex)
......
Finished in 18.1 seconds (0.00s async, 18.1s sync)
1 doctest, 5 tests, 0 failures
```

Pareceu até ter travado certo? O teste demorou 18 segundos. Nosso *delay* esta funcionando corretamente, inclusive o teste passa como deveria. Mas isso causa um problema de fluxo de trabalho. Imagina termos mais 10 clientes que também precisamos fazer o teste de *retry*. Nossa suite de teste ficaria lenta.

Precisamos de uma forma de configurar o delay em nossos testes, para não comprometer nossa performance.

Um minuto de agradecimento a nós do passado, que criamos um mecanismo de configuração. Podemos então fazer o mesmo que fizemos com a configuração da URL base.

<pre class="language-elixir" data-title="lib/coffee_shop/integrations/coffee/client.ex" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.Client do
  alias CoffeeShop.Integrations.Coffee.Response

  def all_hot_coffees(opts \\ []) do
    base_url = Keyword.get(opts, :base_url, "https://api.sampleapis.com")
<strong>    delay_to_retry = Keyword.get(opts, :delay_to_retry, 1000)
</strong>
    middlewares = [
      {Tesla.Middleware.Retry,
<strong>       delay: delay_to_retry,
</strong>       max_retries: 3,
       max_delay: 3000,
       should_retry: fn
         {:ok, %{status: status}} when status in [429] -> true
         {:ok, _} -> false
         {:error, _} -> true
       end}
    ]

    Tesla.client(middlewares)
    |> Tesla.get("#{base_url}/coffee/hot")
    |> Response.build()
  end
end
</code></pre>

Agora basta passar a configuração por parâmetro:

<pre class="language-elixir" data-title="test/integrations/coffee/client_test.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.ClientTest do
  use ExUnit.Case

  alias CoffeeShop.Integrations.Coffee.Client
  alias CoffeeShop.Integrations.Coffee.Response
  alias CoffeeShop.Integrations.Counter

  describe "all_hot_coffees/0" do
    # ...

    test "too much requests", %{bypass: bypass} do
      Counter.start_link(0)

      Bypass.expect(bypass, "GET", "/coffee/hot", fn conn ->
        case Counter.value() do
          0 = _first_call ->
            Counter.increment()
            Plug.Conn.resp(conn, 429, "")

          1 = _first_retry_call ->
            Plug.Conn.resp(conn, 200, "")
        end
      end)

      opts = [
        base_url: "http://localhost:3000",
<strong>        delay_to_retry: 1
</strong>      ]

      assert {:ok, %Response{status: status}} = Client.all_hot_coffees(opts)
      assert status == 200
    end
  end
end
</code></pre>

```
> time mix test
Compiling 1 file (.ex)
......
Finished in 1.0 seconds (0.00s async, 1.0s sync)
1 doctest, 5 tests, 0 failures
```

Teste finalizado em 1 segundo. Muito melhor.

## Refactoring

Por ultimo, vamos dar uma mexida em nosso cliente e deixa-lo melhor. Nossa função de requisição ficou grande e mistura chamada com configuração. Caso eu queira criar uma nova função para requisitar outro *endpoint*, teremos que duplicar a configuração, isso não é bom.&#x20;

Vamos resolver isso extraindo para uma função de criação de client. Vou chama-lo de `new_client/1` . Ela vai ser privada para apenas utilizarmos dentro do modulo. Para configurar o cliente, basta passar as opções de configurações por parâmetro. Sua resposta é um  `Tesla.Client` configurado que poderá ser usado em qualquer requisição.

<pre class="language-elixir" data-title="lib/integrations/coffee/client.ex" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.Client do
  alias CoffeeShop.Integrations.Coffee.Response

<strong>  defp new_client(opts \\ []) do
</strong><strong>    middlewares = [
</strong><strong>      {Tesla.Middleware.Retry,
</strong><strong>       delay: Keyword.get(opts, :delay_to_retry, 1000),
</strong><strong>       max_retries: 3,
</strong><strong>       max_delay: 20_000,
</strong><strong>       should_retry: fn
</strong><strong>         {:ok, %{status: status}} when status in [429] -> true
</strong><strong>         {:ok, _} -> false
</strong><strong>         {:error, _} -> true
</strong><strong>       end}
</strong><strong>    ]
</strong><strong>
</strong><strong>    Tesla.client(middlewares)
</strong><strong>  end
</strong>
  def all_hot_coffees(opts \\ []) do
    base_url = Keyword.get(opts, :base_url, "https://api.sampleapis.com")

    opts
<strong>    |> new_client()
</strong>    |> Tesla.get("#{base_url}/coffee/hot")
    |> Response.build()
  end
end

</code></pre>

Muito melhor. Também gosto de separar a URL base para facilitar leitura.&#x20;

<pre class="language-elixir" data-title="lib/integrations/coffee/client.ex" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.Client do
  alias CoffeeShop.Integrations.Coffee.Response

  defp new_client(opts \\ []) do
    middlewares = [
      {Tesla.Middleware.Retry,
       delay: Keyword.get(opts, :delay_to_retry, 1000),
       max_retries: 3,
       max_delay: 20_000,
       should_retry: fn
         {:ok, %{status: status}} when status in [429] -> true
         {:ok, _} -> false
         {:error, _} -> true
       end}
    ]

    Tesla.client(middlewares)
  end

  def all_hot_coffees(opts \\ []) do
    base_url = Keyword.get(opts, :base_url, base_url())

    opts
    |> new_client()
    |> Tesla.get("#{base_url}/coffee/hot")
    |> Response.build()
  end
  
<strong>  defp base_url(), do: "https://api.sampleapis.com"
</strong>end

</code></pre>

Com isso, temos um código legivel e prático.
