FastGlobalLock (fast_global_lock v0.1.1)
View SourceFastGlobalLock
is a library that builds on top of :global
to minimize the time between locks under contention and to provide a best-effort FIFO locking mechanism.
Key differences from :global
Because FastGlobalLock
builds on :global
, its lock semantics are similar.
There are several key differences to be aware of:
:global | FastGlobalLock |
---|---|
Relies solely on polling with an increasing, random sleep of up to 8 seconds. | Uses inter-process communication to minimize time between locks. |
Which process acquires a lock under contention is fully random. | Orders pending locks and wakes waiting processes in FIFO order. |
Multiple processes can acquire the same lock if they use the same LockRequesterId . | Only one process can hold a lock for a key on any given ndoe. |
Acquires at most one lock no matter how many set_lock/2 calls are made. | Supports lock nesting in its native API. |
Can release the lock only on selected nodes. | Always releases the lock everywhere it was acquired. |
Can extend the lock to more nodes. | If a lock for a key is already held, it can only be nested on the same node set. |
Takes a Retries argument and sleeps for a random time between attempts. | Takes a millisecond timeout . |
The process calling set_lock/2 is monitored for liveness. | A separate linked process is spawned per lock/2 and monitored for liveness. |
Important notes
Increased CPU usage compared with :global
FastGlobalLock
shortens the wall-clock time between locks, but it achieves this by using more CPU time.
A coordination layer on top of :global
inevitably adds some processing overhead.
One particularly bad CPU scenario occurs when there is heavy contention for a key, with some processes locking on a single node while others lock cluster-wide.
To determine if and where the lock is already present, FastGlobalLock
probes the :nodes
one at a time.
It continues this loop without sleeping until it either acquires the lock or discovers that the key is locked elsewhere.
This design is efficient when all requests target roughly the same set of nodes. However, if one process tries to lock 100 nodes while the lock is already held on just one of them, merely locating the lock can become expensive.
Concurrent locks for disjoint node sets
Multiple concurrent locks (by different processes) can be acquired for the same key if they lock on disjoint :nodes
sets.
If processes attempt to acquire locks for :nodes
spanning multiple existing locks, the fairness mechanism is likely to be disturbed.
The fast locking mechanism still works in this scenario.
Note that multiple concurrent locks for the same key cannot be acquired by the same process.
FastGlobalLock
will either nest the existing lock, or raise, depending on the :on_nodes_mismatch
setting.
See lock/2
for more information.
Correctness
FastGlobalLock
inherits most of its correctness guarantees from:global
.- If any mechanism fails, it falls back to polling
:global.set_lock/3
. - While the lock-holder GenServer process is yet to acquire the lock, every callback either returns a timeout value or terminates the server altogether.
- Should a bug in
FastGlobalLock
cause the lock-holder to crash, the lock is released automatically by:global
. The process that requested the lock will receive an exit signal through their link.
Installation
The package can be installed by adding fast_global_lock
to your list of dependencies in mix.exs
:
def deps do
[
{:fast_global_lock, "~> 0.1.1"}
]
end
Summary
Functions
Sets a :global
lock on key
for the current process.
Same as lock/2
, but raises FastGlobalLock.LockTimeoutError
if the lock is not acquired within
the timeout.
Decrements the lock-count for key
.
Acquires a lock on key
and calls fun
while the lock is held.
The lock is released after fun
finishes, whether it returns normally or with
an exception/signal.
Same as with_lock/3
, but raises FastGlobalLock.LockTimeoutError
if the lock is not acquired
within the timeout.
Types
Functions
Sets a :global
lock on key
for the current process.
This function is synchronous and will either return true
if the lock was acquired, or false
if the lock was not acquired within the timeout.
As with :global.set_lock/3
, if a process that holds a lock dies, or the node goes down,
the locks held by the process are released.
If the lock is already held by the current process, lock/2
will return true
and increment
the lock-count. While the current process is alive, it must call unlock/2
the same number
of times to release the lock.
Important
If a key is already locked by this process, FastGlobalLock
will not allow another lock to be
acquired, even if given a different set of nodes. See :on_nodes_mismatch
option.
Options
:timeout
- a millisecond timeout after which the operation will fail. Defaults to:infinity
:nodes
- nodes to lock on. Defaults toNode.list([:this, :visible])
.:on_nodes_mismatch
- what to do if the lock is already held by this process on a different set of nodes::ignore
- increment the lock-count without raising an error:raise
- raiseFastGlobalLock.NodesMismatchError
:raise_if_disjoint
- raiseFastGlobalLock.NodesMismatchError
if the requested:nodes
have no overlap with the existing lock's:nodes
. Otherwise increment the lock-count. This is the default setting.
Example
# Acquire the first lock FastGlobalLock.lock(:foo, nodes: [:a, :b]) # Raises an error because of the nodes mismatch # This would also happen if we didn't provide the `:nodes` option and the cluster membership # changed between the first and second lock FastGlobalLock.lock(:foo, nodes: [:a, :b, :c], on_nodes_mismatch: :raise) # Won't fail as there's an overlap [:a] node with the existing lock's `nodes: [:a, :b]` FastGlobalLock.lock(:foo, nodes: [:a, :c], on_nodes_mismatch: :raise_if_disjoint) # Raises an error because the nodes are disjoint with the existing lock's `nodes: [:a, :b]` FastGlobalLock.lock(:foo, nodes: [:c, :d], on_nodes_mismatch: :raise_if_disjoint) # Won't fail no matter what the requested `:nodes` are FastGlobalLock.lock(:foo, nodes: [:c, :d], on_nodes_mismatch: :ignore)
Same as lock/2
, but raises FastGlobalLock.LockTimeoutError
if the lock is not acquired within
the timeout.
Decrements the lock-count for key
.
If the lock-count is decremented to 0, the lock is synchronously released.
Returns true
if the lock-count was decremented, false
if the lock is not held by the current
process.
@spec with_lock(key :: any(), timeout() | options(), (-> result)) :: {:ok, result} | {:error, :lock_timeout} when result: any()
Acquires a lock on key
and calls fun
while the lock is held.
The lock is released after fun
finishes, whether it returns normally or with
an exception/signal.
Returns {:ok, result}
if the lock was acquired and fun
returned result
,
or {:error, :lock_timeout}
if the lock was not acquired within the timeout.
See lock/2
for more details on options taken by this function.
@spec with_lock!(key :: any(), timeout() | options(), (-> result)) :: result | no_return() when result: any()
Same as with_lock/3
, but raises FastGlobalLock.LockTimeoutError
if the lock is not acquired
within the timeout.