# Criando uma requisição assíncrona

Primeiro iremos criar uma nova requisição. Utilizaremos o mesmo recurso para fins de estudo, mas a estratégia de execução será diferente.

* Precisamos da atualização dos cafés quentes;
* Temos a limitação de 50 requisições por dia no *Rate Limit*;
* Não precisamos ter uma resposta imediata, isso pode ser atualizado quando der.

Tendo isso em mente, criaremos uma requisição que bate no mesmo endpoint dos cafés quentes, porém, não iremos esperar uma resposta imediata e sim, um :ok, nos avisando que o agendamento esta pronto.&#x20;

<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, base_url())

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

<strong>  def all_hot_coffees_async(opts \\ []) do
</strong><strong>    
</strong><strong>  end
</strong>
  defp base_url(), do: "https://api.sampleapis.com"
end

</code></pre>

usei o mesmo nome da função, com adição do sufixo `_assync`, indicando assincronicidade.  Isso quer dizer, não esperamos que isso seja feito agora e sim, de forma assincrona.

Diferente da primeira função, na segunda precisamos agendar um [Job ](https://hexdocs.pm/oban/Oban.Job.html)para ser executado. Para isso, precisamos primeiro do [Worker](https://hexdocs.pm/oban/Oban.Worker.html) que irá processar nosso [Job](https://hexdocs.pm/oban/Oban.Job.html). Vamos criar o *worker* com o nome `HotCoffeesWorker` que fara a sincronia de forma assíncrona para nos.

A estrutura do nosso modulo é bem simples, utilizaremos a macro `use Oban.Worke`r e precisaremos implementar a função `perform/1` .

{% code title="lib/coffee\_shop/integrations/coffee/hot\_coffees\_worker.ex" lineNumbers="true" %}

```elixir
defmodule CoffeeShop.Integrations.Coffee.HotCoffeesWorker do
  use Oban.Worker

  @impl Oban.Worker
  def perform(%Oban.Job{args: _args}) do
    :ok
  end
end

```

{% endcode %}

A função `perform/1` será executada quando um job rodar. Precisamos que ela realize a chamada para o serviço externo.  Para isso, vamos reaproveitar a função sincrona criada no cliente, `Client.all_hot_coffees/1.`

<pre class="language-elixir" data-title="lib/coffee_shop/integrations/coffee/hot_coffees_worker.ex" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.HotCoffeesWorker do
  use Oban.Worker

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

  @impl Oban.Worker
  def perform(%Oban.Job{args: %{"opts" => opts}}) do
<strong>    opts = to_list(opts)
</strong>    
<strong>    case Client.all_hot_coffees(opts) do
</strong><strong>      {:ok, %Response{status: 200, body: _body}} -> :ok
</strong><strong>      {:ok, %Response{status: status, body: _body}} -> {:error, "status: #{status}"}
</strong><strong>      error -> error
</strong>    end
  end
  
<strong>  defp to_list(map) do
</strong><strong>    Enum.map(map, fn {key, value} -> {String.to_existing_atom(key), value} end)
</strong><strong>  end
</strong>end
</code></pre>

O Oban salva os parâmetros como map, devido a não suportar uma lista. Por isso que ao receber o opts convertemos ele novamente para lista e o processamento continua igual o original utilizanod o *Keyword*.

O `case/1`  controla o que esperamos. Nesse caso, sera apenas um sucesso quando recebermos um **status 200**. Fora isso, queremos que seja um erro.

Nosso *worker* está pronto, agora precisamos agendar a execução. Para isso utilizaremos a função `Oban.insert/1` que espera um *Job*. Para conseguirmos o *Job* de forma fácil, utilizaremos a macro em nosso modulo *worker* e executaremos a função `HotCoffeesWorker.new/1` que retorna um *Job* já configurado para nosso *Worker*. Vamos replicar isso em nosso cliente.

<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
<strong>  alias CoffeeShop.Integrations.Coffee.HotCoffeesWorker
</strong>  
  # ...

  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>  def all_hot_coffees_async(opts \\ []) do
</strong><strong>    opts
</strong><strong>    |> Map.new(fn option -> option end)
</strong><strong>    |> HotCoffeesWorker.new()
</strong><strong>    |> Oban.insert()
</strong><strong>  end
</strong>
  defp base_url(), do: "https://api.sampleapis.com"
end
</code></pre>

{% hint style="info" %}
Na linha 18, convertemos a lista em Map. Como informamos que aconteceria na criação do worker.
{% endhint %}

Ao rodar a função `all_hot_coffees_async/1` receberemos um `{:ok, _job}` de resposta e não mais a estrutura `%Response{}`, isso acontece porque estamos criando um novo *Job* e não mais realizando uma requisição para o serviço. Vamos criar um teste comprovando nossa ideia.

<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
<strong>  use Oban.Testing, repo: CoffeeShop.Repo
</strong>
  alias CoffeeShop.Integrations.Coffee.Client
  alias CoffeeShop.Integrations.Coffee.HotCoffeesWorker
  alias CoffeeShop.Integrations.Coffee.Response
  alias CoffeeShop.Integrations.Counter

  # ...
  
<strong>  describe "all_hot_coffees_async/0" do
</strong><strong>    test "schedule a job" do
</strong><strong>      opts = [
</strong><strong>        base_url: "http://localhost:3000",
</strong><strong>        retry_delay: 1
</strong><strong>      ]
</strong><strong>
</strong><strong>      assert {:ok, _job} = Client.all_hot_coffees_async(opts)
</strong><strong>
</strong><strong>      # Verificando se foi agendado com sucesso
</strong><strong>      assert_enqueued(
</strong><strong>        worker: HotCoffeesWorker,
</strong><strong>        args: %{"base_url" => "http://localhost:3000", "retry_delay" => 1}
</strong><strong>      )
</strong><strong>    end
</strong>  end
end
</code></pre>

Você já pode rodar os testes do nosso cliente

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

```
> mix test test/coffee_shop/integrations/coffee/client_test.exs
....
Finished in 1.0 seconds (0.00s async, 1.0s sync)
4 tests, 0 failures
```

Agendamento realizado com sucesso. Mas precisamos de mais garantias. Nosso Rate Limit é de 24h. Isso quer dizer que precisamos garantir que essa execução só irá ser executada em 24h certo? No teste, nao temos essa garantia ainda, temos que adiciona-la.

<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
  use Oban.Testing, repo: CoffeeShop.Repo

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

  # ...

  describe "all_hot_coffees_async/0" do
    test "schedule a job" do
      opts = [
        base_url: "http://localhost:3000",
        retry_delay: 1
      ]

      assert {:ok, _job} = Client.all_hot_coffees_async(opts)

<strong>      in_a_day = DateTime.add(DateTime.utc_now(), 3600 * 24, :second)
</strong>
      assert_enqueued(
        worker: HotCoffeesWorker,
        args: %{"base_url" => "http://localhost:3000", "retry_delay" => 1},
<strong>        scheduled_at: in_a_day
</strong>      )
    end
  end
end

</code></pre>

O próprio Oban nos disponibiliza na função `assert_enqueued` o `scheduled_at`, para confirmarmos para quando foi agendado. Com isso, criamos a variavel in\_a\_day  com a utilização do `DateTime` para adicionarmos o tempo a partir de agora + 24h e adicionamos na `assertion`. Vamos rodar e ver o que acontece.

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

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

  1) test all_hot_coffees_async/0 schedule a job (CoffeeShop.Integrations.Coffee.ClientTest)
     test/coffee_shop/integrations/coffee/client_test.exs:73
     Expected a job matching:
     
     %{
       args: %{"base_url" => "http://localhost:3000", "retry_delay" => 1},
       worker: CoffeeShop.Integrations.Coffee.HotCoffeesWorker,
       scheduled_at: ~U[2024-04-17 13:44:40.309948Z]
     }
     
     to be enqueued. Instead found:
     
     [
       %{
         args: %{"base_url" => "http://localhost:3000", "retry_delay" => 1},
         worker: "CoffeeShop.Integrations.Coffee.HotCoffeesWorker",
         scheduled_at: ~U[2024-04-16 13:44:40.294598Z]
       }
     ]
     
     code: assert_enqueued(
     stacktrace:
       test/coffee_shop/integrations/coffee/client_test.exs:83: (test)

..
Finished in 1.2 seconds (0.00s async, 1.2s sync)
4 tests, 1 failure
```

A há, é um ótimo teste para se ter, não é? Esperamos é rodar o *job* em **2024-04-17,** mas está sendo executado um dia antes **2024-04-16**. Isso quer dizer, esta rodando logo quando é agendado.&#x20;

Não é o comportamento que esperamos. Isso está acontecendo porque realmente não configuramos essa etapa. Devemos configurar isso quando o *job*. Em nosso cliente é inserido o *job* e lá vamos adicionar essa opção:

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

  # ...
  
  def all_hot_coffees_async(opts \\ []) do
<strong>   in_a_day = DateTime.add(DateTime.utc_now(), 3600 * 24, :second)
</strong>
    opts
    |> Map.new(fn option -> option end)
<strong>    |> HotCoffeesWorker.new(scheduled_at: in_a_day)
</strong>    |> Oban.insert()
  end

  # ...
end
</code></pre>

Criado a regra na linha 9 e adicionado a opção na linha 12. Agora rodaremos o teste novamente.

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

```
mix test test/coffee_shop/integrations/coffee/client_test.exs
Compiling 1 file (.ex)
....
Finished in 1.1 seconds (0.00s async, 1.1s sync)
4 tests, 0 failures
```

Estamos atendendo a regra que precisamos seguir com o *rate limit* de longa duração.

Em nosso teste, não temos garantia de que a execução do perform funciona, apenas garantimos o agendamento. Precisamos saber se conseguimos rodar o que foi agendado. Para isso usaremos a função `perform_job/2` que executará o job agendado.

<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
  use Oban.Testing, repo: CoffeeShop.Repo

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

<strong>  setup do
</strong><strong>    bypass = Bypass.open(port: 3000)
</strong><strong>    {:ok, bypass: bypass}
</strong><strong>  end
</strong>
  describe "all_hot_coffees/0" do
    test "respond a list of hot coffees", %{bypass: bypass} do
      # ...
    end

    test "service is crashed", %{bypass: bypass} do
      # ...
    end

    test "too much requests", %{bypass: bypass} do
      # ...
    end
  end

  describe "all_hot_coffees_async/0" do
    test "schedule a job" do
<strong>      Bypass.expect_once(bypass, "GET", "/coffee/hot", fn conn ->
</strong><strong>        Plug.Conn.resp(conn, 200, "")
</strong><strong>      end)
</strong>      
      opts = [
        base_url: "http://localhost:3000",
        retry_delay: 1
      ]

      assert {:ok, _job} = Client.all_hot_coffees_async(opts)

      in_a_day = DateTime.add(DateTime.utc_now(), 3600 * 24, :second)

      assert_enqueued(
        worker: HotCoffeesWorker,
        args: %{"base_url" => "http://localhost:3000", "retry_delay" => 1},
        scheduled_at: in_a_day
      )
      
<strong>      assert %{success: 1} =
</strong><strong>               Oban.drain_queue(queue: :default, with_scheduled: true, with_safety: false)
</strong>    end
  end
end

</code></pre>

Adicionamos uma nova etapa na linha 50 que executa o job agendado. Sua resposta vem com a estrutura de map dos seguintes dados:

```elixir
%{failure: _, snoozed: _, success: _}
```

Para nosso teste passar, a execução deve ser um sucesso. por isso nossa resposta está com o pattern matching `%{success: 1}`.&#x20;

Rode seu teste e veja o resultado.

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

```
> mix test test/coffee_shop/integrations/coffee/client_test.exs   
....
Finished in 1.1 seconds (0.00s async, 1.1s sync)
4 tests, 0 failures
```

Mais bonito que uma geladeira inox side-by-side com dispenser de gelo.
