sezaru

sezaru

Case to use or not use :infinity as timeout for calls

Hello,

Setting the timeout value in my GenServer calls was always something that I found uncomfortable, it seemed to me that 5 seconds default was kinda a “random” number and I was not sure what number I needed to use.

At the same time, I was scared of using :infinity for it since you rarely see an example using it, so I thought it was not safe (I thought you would be stuck if the callee died or something like that).

Well, looking deeply I found this link Thoughts on when to use ‘infinity’ timeouts for gen_server:call and friends. They do discuss the call default timeout and says that it should be :infinity by default.

After that, I did some tests and indeed it seems to me to be very safe to use it as the default (and only use a timed timeout when it really makes sense). It fixed a lot of issues I had with timeouts when I changed the backend machine processing power which would often trigger these timeouts.

So, my question is a two-part one (sorry for that), the first part is, what is your opinion about that? Maybe Genserver documentation should be more clear about it (If it is I couldn’t find it sincerely)? Maybe we should have :infinity as the default timeout for calls as the link suggests?

The second part of the question is in regard of other timeouts configurations in the system that too are not really clear if they are safe to use :infinity or not.

For example, Ecto.Repo, you have the :timeout parameter for your config, this is what the documentation tells about it:

The time in milliseconds to wait for the query call to finish. :infinity will wait indefinitely (default: 15000)

As you can see, it mentions :infinity, but it is not clear (at least to me) if the query call is a Genserver.call or it is the call to the database server. If it is the first, I would consider safe to use :infinity since if the callee dies, we will not be stuck. But if it is referencing the database server, then my guess is that it could simply crash/disappear/whatever and it would never return from it, basically being locked in this call forever.

So, the second part of my question is, is it safe to use :infinity for the case of Ecto.Repo as an example? Do you know other libs that would not be?

Thank you very much.

Most Liked

lucaong

lucaong

The thing is, speaking about asynchronous calls in general and not referring to GenServer, it makes sense to explicitly timeout when something takes more than reasonable. With GenServer though, if the call times out, the caller fails, but the server is still running and trying to produce the result even after the timeout. In other words, the deadlock would still be there, as the GenServer would be still blocked. If one really wants to free up resources when an operation takes too long, a custom timeout logic on the GenServer side is better than a timeout on the caller.

Conversely, using a timeout of :infinity would at least ensure that if there is an unreasonable delay, it surfaces immediately. The right action to take is then to enforce a timeout logic that cleans up resources, which is not what the GenServer.call/3 timeout does.

The GenServer timeout was absolutely necessary back when gen_server could crash without the caller knowing about that. Nowadays it’s generally not the case anymore.

asummers

asummers

IMO :infinity an an antipattern. It could be easily replaced with 3 hours and have the same effect. Should this run for 3h? Probably not. So why say it can run for infinity? If I have an Ecto query that has to e.g. take a whole table lock, and it can’t get the lock (say for TRUNCATE), giving it an infinity timeout will cause it to hold a DB worker forever. Get enough of these and you have major resource contention on your DB.

asummers

asummers

If I have a single GenServer there’s only one message queue, so it exhibits the same resource contention as a DB. You can model this differently, of course, but naively using :infinity everywhere has the potential to deadlock your whole app. There are cases where you do need :infinity but I can’t think of any off the top of my head where saying 3 weeks or some equally silly large number would be less appropriate.

lucaong

lucaong

Well, I understand that timeouts in general are a good thing, but the GenServer.call timeout gives you no chance of cleaning up, and hides the real problem.

Suppose we are in a scenario in which the GenServer operation is very slow. For the sake of the argument, let’s say it hangs forever. If the caller enforces a timeout with GenServer.call/3, even after the timeout elapses the GenServer is still hanging. Any subsequent call will be queued in the mailbox of the hanging GenServer, which keeps growing unbound. The real problem is not solved, because the GenServer is not released.

Using a timeout of :infinity would block the caller forever. It is also not a great course of action, but it reflects the real performance of the GenServer, propagating backpressure at least on that specific caller. I agree that it’s not the solution, but my point is that setting a timeout is not a solution either.

The real solution in such a case is a use-case specific timeout logic inside the GenServer, that knows how to cleanup resources.

lucaong

lucaong

Here is a code example of what I mean. Let’s simulate a slow call (also printing a message every second while waiting):

defmodule Slow do
  def call(seconds) do
    for i <- (1..seconds) do
      IO.puts("Waiting #{i}...")
      Process.sleep(1_000)
    end
  end
end

Now create a GenServer setting a 3 seconds call timeout:

defmodule One do
  def start_link(), do: GenServer.start_link(__MODULE__, [], [])

  def hang(pid, seconds \\ 10),
    do: GenServer.call(pid, {:hang, seconds}, 3000)

  def init(_), do: {:ok, nil}

  def handle_call({:hang, seconds}, _from, state) do
    reply = Slow.call(seconds)
    {:reply, reply, state}
  end
end

If we call One.hang(pid), it will hang until the 3 seconds timeout elapses, then error. As we can see from the printed messages though, the slow operation is still going on, and further calls will just engulf the inbox more and more:

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

One.hang()
# Waiting 1...
# Waiting 2...
# Waiting 3...
# ** (exit) exited in: GenServer.call(#PID<0.165.0>, {:hang, 30}, 5000)
#     ** (EXIT) time out
#     (elixir) lib/gen_server.ex:1009: GenServer.call/3
# Waiting 4...
# Waiting 5...

One.hang()
# Waiting 6...
# Waiting 7...
# Waiting 8...
# ** (exit) exited in: GenServer.call(#PID<0.165.0>, {:hang, 30}, 5000)
#     ** (EXIT) time out
#     (elixir) lib/gen_server.ex:1009: GenServer.call/3
# Waiting 9...
# Waiting 10...
# Waiting 1...
# Waiting 2...
# Waiting 3...

The GenServer.call timeout is not really helping, as it only stops the caller, not the callee. What would work is to implement logic on the callee side to stop the slow operation and cleanup if a timeout elapses:

defmodule Two do
  def start_link(), do: GenServer.start_link(__MODULE__, [], [])

  def hang(pid, seconds \\ 10),
    do: GenServer.call(pid, {:hang, seconds}, :infinity)

  def init(_), do: {:ok, nil}

  def handle_call({:hang, seconds}, from, state) do
    task = Task.async(Slow, :call, [seconds])

    case Task.yield(task, 3000) || Task.shutdown(task) do
      {:ok, reply} -> {:reply, reply, state}
      nil -> {:stop, :timeout, state}
    end
  end
end

In this case, the slow call is wrapped in a Task that is terminated when the 3 seconds timeout elapses:

Two.hang(pid)
# Waiting 1...
# Waiting 2...
# Waiting 3...

# 18:57:12.341 [error] GenServer #PID<0.165.0> terminating
# ** (stop) time out
# Last message (from #PID<0.104.0>): {:hang, 30}
# ...
# ** (EXIT from #PID<0.104.0>) shell process exited with reason: time out

No more "Waiting #..." messages are logged, confirming that the slow Task is terminated after the timeout elapses.

Where Next?

Popular in Questions Top

tduccuong
Hi, is there any work on GUI with Elixir, that is similar to Electron/Javascript? My idea is to bundle Phoenix and BEAM into a single se...
New
Werner
Hi, I’m using Ubuntu 18.04 and after updating to OTP-24.0 yesterday i have this warning when I run “mix local.hex”: 14:57:30.512 [warn] ...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod -- where is this set? Thanks.
New
Kagamiiiii
Student &amp; New to elixir. Nice language. I want to convert a english character, e.g. “a”, which is stored in a variable, to it’s asci...
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
chewm
Hi guys, nice to meet you to the whole forum, I’m new here, I’m trying to configure visual studio code for elixir, right now the intellis...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
idi527
I’ve been re-reading swift book again and noticed that multiline strings there don’t have a trailing line break, unlike in elixir iex(2)...
New

Other popular topics Top

peerreynders
Manning 2016 Halloween weekend sale via Deal of the Day Friday, October 28 - Half off all MEAPs - code WM102816LT Saturday, October 29 ...
326 29600 154
New
malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
vac
Hi, I'm quite new in Elixir and I'm trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and ...
New
chrismccord
As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
New
danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 27727 240
New
Nvim
Elixir appears to be a superior language to Python. I don’t see any advantage of Python over Elixir. Are there any?
New

We're in Beta

About us Mission Statement