Locksmith (Locksmith v1.0.0) View Source

Queue-free/gen_server-free/process-free locking mechanism built for high concurrency.

This allows for the use of locks in hot-code-paths without being bottlenecked by processes message queues.

Usage

Simply call the transaction/2,3,4 function with a locking key, and a function to apply, the function is guaranteed to be excluded while exclusively locking the given key.

Examples

Given two functions, one being slower than the other, the ordering isn't guaranteed if both are run by separate process, and if they have some side effects, such as update cache values, this could be undesirable.

iex> myself = self()
iex> first_function = fn ->
...>   :timer.sleep(2_000)
...>   send myself, :first_function
...> end
iex> second_function = fn ->
...>   :timer.sleep(1_000)
...>   send myself, :second_function
...> end
iex> spawn first_function
iex> spawn second_function
iex> :ok = receive do
...>   :first_function -> :error
...>   :second_function -> :ok
...> end
iex> :ok = receive do
...>   :first_function -> :ok
...>   :second_function -> :error
...> end

Using Elixir.Locksmith you can ensure they don't run at the same time by locking both under the same key, note that ordering isn't guaranteed per-se as the lock is given on a first-come-first-serve bases.

iex> myself = self()
iex> first_function = fn ->
...>   :timer.sleep(2_000)
...>   send myself, :first_function
...> end
iex> second_function = fn ->
...>   :timer.sleep(1_000)
...>   send myself, :second_function
...> end
iex> spawn fn -> Locksmith.transaction("somekey", first_function) end
iex> spawn fn -> Locksmith.transaction("somekey", second_function) end
iex> :ok = receive do
...>   :first_function -> :ok
...>   :second_function -> :error
...> end
iex> :ok = receive do
...>   :first_function -> :error
...>   :second_function -> :ok
...> end

Notice that after using Locksmith the first transaction which acquired the lock first managed to execute even before the second transaction could start, since the second transaction was locked until the key is released.

Implementation

When starting this module, it'll initiate an Eternal process, which will handle creating and maintaining a long live ETS table that is not bound by any process and lives across the applications lifecycle. This is required since ETS tables are bound by the process that creates them and when the process dies the ETS table is deleted unless their an "heir" to the table, Eternal handles retaining the table as long as our app lives.

Internally this module utilizes :ets.update_counter/4 function, which provides us with an atomic and isolated updates to counter in ETS tables. Given any transaction with a lock key, a lock is "acquired", this is achieved by calling :ets.update_counter/4, defaulting the lock key to counter of value 0 if not found and then incrementing it by 1, if the counter after update is equal to 1 then the lock is acquired otherwise the lock isn't acquired.

The increment operation is done with a threshold and set_value set to 2 forcing the counter to never exceed the value 2. This means each lock counter is set to a three state value, 0 lock is free, 1 lock has been acquired, 2 lock was acquired by someone else.

After the function is applied the lock is "released" by resetting it's value to 0, this is done by increment by 1, while having threshold and set_value to 0 each, forcing the counter to reset atomically.

If the acquire operation fails (returns false), the current process is blocked via a receive/1 operation, and before running the receive/1 operation it'll sent itself a delayed message using Process.send/4, once it receives the message it sent itself, it'll attempt to acquire the lock again, this behaviour is done recursively.

Link to this section Summary

Functions

Given a locking key, an anonymous function of arity zero, lock the given key and execute the function, then release the key. If the key is already locked then retry to lock the key and run again after some delay. This is achieved by blocking the caller process using a receive/1 call coupled with Process.send_after/4.

Alternative to transaction/2 that takes an anonymous function of any arity and a list of arguments for it.

Alternative to transaction/2 that takes an MFA instead of an anonymous function.

Link to this section Functions

Specs

transaction(any(), (() -> any())) :: any()

Given a locking key, an anonymous function of arity zero, lock the given key and execute the function, then release the key. If the key is already locked then retry to lock the key and run again after some delay. This is achieved by blocking the caller process using a receive/1 call coupled with Process.send_after/4.

Link to this function

transaction(key, fun, args)

View Source

Specs

transaction(any(), (... -> any()), list()) :: any()

Alternative to transaction/2 that takes an anonymous function of any arity and a list of arguments for it.

Link to this function

transaction(key, mod, fun, args)

View Source

Specs

transaction(any(), module(), atom(), list()) :: any()

Alternative to transaction/2 that takes an MFA instead of an anonymous function.