lud/mutex

`Mutex.under` does not release lock upon crashes

Closed this issue · 4 comments

Hi! First off, thanks for this excellent library. Mutex.under_all has saved my ass more than once.

Now if I load this module into IEx:

defmodule Mutest do
  def foo do
    Mutex.under(:some_mutex, :key, fn ->
      exit(:hello)
    end)
  end
end

And then call it:

iex(2)> Mutest.foo()
** (exit) :hello
    iex:4: anonymous fn/0 in Mutest.foo/0
    (mutex 1.1.3) lib/mut.ex:262: Mutex.apply_with_lock/3
iex(2)> Mutest.foo()

*IEx freezes*

This is a problem when a process crashes without properly throwing. We've seen this happen in practice (though not in mutexed code) when a database pool timed out and a calling process crashed because of it. I'm not a deep enough BEAM guru to understand exactly when something comes a :throw and when something is an :exit, but this was an :exit

We found that Mutex had the same problem when we tried to use it to replace another library that froze upon crashing.

If this is indeed considered a bug and not by design, then it might be a matter as simple as adding an :exit clause to this catch, but not sure.

Also, my compliments on the mut.ex filename.

lud commented

Hi,

For some reason I was not notified. I'll look into it.

lud commented

Hi again,

First, thanks for the kind words.

Regarding your problem, it is because there is a special behaviour in iex that prevents the process to actually exit.

If you modify your module to print the pid like that:

defmodule Mutest do
  def foo do
    Mutex.under(:some_mutex, :key, fn ->
      IO.puts "exiting from #{inspect self}"
      exit(:hello)
    end)
  end
end

You can run the same session:

iex(2)> Mutex.start(:some_mutex)
{:ok, #PID<0.251.0>}
iex(3)> self()
#PID<0.235.0>
iex(4)> Mutest.foo
exiting from #PID<0.235.0>
** (exit) :hello
    iex:5: anonymous fn/0 in Mutest.foo/0
    (mutex 1.2.0) lib/mut.ex:262: Mutex.apply_with_lock/3
iex(4)> self()
#PID<0.235.0>
iex(5)> Mutest.foo()
=> hangs

And you see that the two calls to self() returns the same pid. The process did not actually exit.

If you run the code in a "normal" process with spawn(fn -> Mutest.foo() end), it works as expected.

I added this test to show that the current implementation behaves well when exit/1 is called from the callback.

The mutex process monitors the processes that lock keys, so whenever they exit, their locks are released. This is why I do not catch exits, indeed, by design.

I wouldn't like to add an :exit clause to the catch as it should not be needed. Could you provide a simple example (or best, a failing test) of a lock not being released upon exit, that fails even when not called from iex ?

lud commented

I thought about it and indeed, if you catch the exit outside of the callback then the problem arises, just as in iex. Is that what you do in your app?

I am not sure if mutex should handle this case. On one hand, it should because the under/4 function. On the other hand, exits are special. I will ask on the forums.

lud commented

Well,

I just replaced the wrapper with that:

  defp apply_with_lock(mutex, lock, fun) do
    fun.(lock)
  after
    release(mutex, lock)
  end

So it works as intended, even with exits, and without the bookkeeping for rethrowing :)