Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mint adapter deadlocks GenServer using it at handle_continue call #398

Closed
sezaru opened this issue Jun 28, 2020 · 6 comments
Closed

Mint adapter deadlocks GenServer using it at handle_continue call #398

sezaru opened this issue Jun 28, 2020 · 6 comments
Labels
mint Issues related to mint adapter

Comments

@sezaru
Copy link
Contributor

sezaru commented Jun 28, 2020

Hello, if Mint adapter is used during GenServer handle_continue call, and someone calls the Genserver during it, for some reason it always fails with {:error, :unknown} message and them deadlocks the GenServer itself.

Below is a sample code to trigger it:

defmodule Server do
  use GenServer

  def start_link, do: GenServer.start_link(__MODULE__, :no_args)

  @impl GenServer
  def init(:no_args), do: {:ok, :no_state, {:continue, :sleep}}

  @impl GenServer
  def handle_continue(:sleep, state) do
    middleware = [
      {Tesla.Middleware.BaseUrl, "https://www.google.com"}
    ]

    adapter = {Tesla.Adapter.Mint, timeout: :timer.seconds(10)}

    client = Tesla.client(middleware, adapter)

    Tesla.get(client, "/") |> IO.inspect()

    IO.puts("Finished waiting")

    {:noreply, state}
  end

  @impl GenServer
  def handle_call(:some_call, _, state) do
    IO.puts("Called")

    {:reply, :ok, state}
  end

  @impl GenServer
  def handle_info({_, :ok}, state), do: {:noreply, state}
  def handle_info({:DOWN, _, :process, _, :normal}, state), do: {:noreply, state}
end

{:ok, pid} = Server.start_link()

GenServer.call(pid, :some_call, :infinity)

As you can see, we initialize the GenServer and then run :some_call call.
:some_call will deadlock and never return a result.

Note that you need to call :some_call during GenServer initialization, if you call after it, it works as expected, so my recommendation is to simply copy and paste this whole code into IEX to trigger it.

@alex-strizhakov
Copy link
Contributor

Can't reproduce it 😒
Can you try to use it with :passive mode?

adapter = {Tesla.Adapter.Mint, timeout: :timer.seconds(10), mode: :passive}

@sezaru
Copy link
Contributor Author

sezaru commented Jun 29, 2020

With passive mode it works great!

@sezaru
Copy link
Contributor Author

sezaru commented Jun 29, 2020

I wonder why you can't reproduce, I tried in three machines here (2 with ubuntu, one with gentoo) and some versions of erlang and elixir and I was able to reproduce in every single one.

Just to remove any variable from the test, I uploaded a github project which reproduces the issue, if you don't mind, can you test with it?

The link is here: https://github.com/sezaru/test_tesla

You only need to run iex inside it and run commant TestTesla.run.

@sezaru
Copy link
Contributor Author

sezaru commented Jun 29, 2020

I created a new issue in Mint github page since I was able to reproduce it using mint directly without Tesla

@sezaru
Copy link
Contributor Author

sezaru commented Jun 30, 2020

Hey @alex-strizhakov talking with the Mint devs, we figured out what is happening.

Basically the function receive_message uses a receive do expecting the Mint HTTP response:

    defp receive_message(conn, %{mode: :active} = opts) do
      receive do
        message ->
          HTTP.stream(conn, message)
      after
        opts[:timeout] -> {:error, :timeout}
      end
    end

But if I do a GenServer.call before the HTTP message arrives, this receive do will get the GenServer.call message instead, send it to HTTP.stream and return :unknown since it expects the HTTP response. The GenServer.call will never receive a reply and will timeout or be blocked forever if used with :infinity.

A solution would be to pattern match the possible mint responses, for example:

    defp receive_message(conn, %{mode: :active} = opts) do
      receive do
        {:ssl, {:sslsocket, _, _}, _} = message ->
          HTTP.stream(conn, message)
      after
        opts[:timeout] -> {:error, :timeout}
      end
    end

Obviously this needs to handle all possible valid messages, but it fixes the issue for my case.

@teamon
Copy link
Member

teamon commented Jul 22, 2020

Closing since #399 has been merged.

@teamon teamon closed this as completed Jul 22, 2020
@teamon teamon added the mint Issues related to mint adapter label Jul 22, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
mint Issues related to mint adapter
Projects
None yet
Development

No branches or pull requests

3 participants