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
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
.
Specs
Alternative to transaction/2
that takes an anonymous function of any arity and a list of arguments for it.
Specs
Alternative to transaction/2
that takes an MFA instead of an anonymous function.