├── rebar.lock ├── rebar.config ├── .gitignore ├── .travis.yml ├── src ├── sleeplocks.app.src └── sleeplocks.erl ├── LICENSE └── README.md /rebar.lock: -------------------------------------------------------------------------------- 1 | []. 2 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {plugins, [rebar3_hex]}. 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /.rebar 3 | /.rebar3 4 | /doc 5 | /ebin 6 | *.crashdump 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | otp_release: 3 | - 21.0 4 | - 20.3 5 | - 19.3 6 | - 18.3 7 | branches: 8 | only: 9 | - master 10 | -------------------------------------------------------------------------------- /src/sleeplocks.app.src: -------------------------------------------------------------------------------- 1 | {application, sleeplocks, 2 | [{description, "BEAM friendly spinlocks for Elixir/Erlang"}, 3 | {vsn, "1.1.1"}, 4 | {modules, []}, 5 | {registered, []}, 6 | {applications, [kernel, stdlib]}, 7 | {maintainers, ["Isaac Whitfield"]}, 8 | {licenses, ["MIT"]}, 9 | {links, [{"Github", "https://github.com/whitfin/sleeplocks"}]}]}. 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Isaac Whitfield 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sleeplocks 2 | [![Build Status](https://img.shields.io/travis/whitfin/sleeplocks.svg?label=unix)](https://travis-ci.org/whitfin/sleeplocks) [![Hex.pm Version](https://img.shields.io/hexpm/v/sleeplocks.svg)](https://hex.pm/packages/sleeplocks) [![Documentation](https://img.shields.io/badge/docs-latest-blue.svg)](https://hexdocs.pm/sleeplocks/) 3 | 4 | This library is designed to provide simple locking mechanisms in Erlang/Elixir, similar to 5 | how spinlocks work in other languages - except using messages to communicate locking. 6 | 7 | This is useful for libraries which require lock synchronization, without having to roll your 8 | own (however simple). Locks can be held by arbitrary numbers of process, making it possible 9 | to implement various throttling mechanisms. 10 | 11 | Best of all, this library is tiny! It builds upon basic OTP principles to implement lock 12 | behaviour via simple processes and message passing. 13 | 14 | ## Installation 15 | 16 | ### Rebar 17 | 18 | Follow the instructons found [here](https://hex.pm/docs/rebar3_usage) to configure your 19 | Rebar setup to use Hex as a dependency source, then you can grab it directly: 20 | 21 | ```erlang 22 | {deps,[ 23 | % pulls the latest version 24 | sleeplocks, 25 | % to pull the latest version from github 26 | {sleeplocks, {git, "git://github.com/whitfin/sleeplocks.git"}} 27 | ]}. 28 | ``` 29 | 30 | ### Mix 31 | 32 | To install it for your project, you can pull it directly from Hex. Rather 33 | than use the version shown below, you can use the latest version from 34 | Hex (shown at the top of this README). 35 | 36 | ```elixir 37 | def deps do 38 | [{:sleeplocks, "~> 1.0"}] 39 | end 40 | ``` 41 | 42 | ## Usage 43 | 44 | Snippets below contain sample usage in both Erlang and Elixir, and cover most of the small 45 | API space offered by `sleeplocks`. For a more complete example, scroll down! 46 | 47 | ### Erlang 48 | 49 | ```erlang 50 | % create a new single lock (with a name) 51 | 1> sleeplocks:new(1, [{name, {local, my_lock}}]). 52 | {ok,<0.179.0>} 53 | 54 | % take ownership of the lock 55 | 2> sleeplocks:acquire(my_lock). 56 | ok 57 | 58 | % release the current hold on a lock 59 | 3> sleeplocks:release(my_lock). 60 | ok 61 | 62 | % attempt to acquire a lock (which will succeed) 63 | 4> sleeplocks:attempt(my_lock). 64 | ok 65 | 66 | % now that it's taken, other attempts will fail 67 | 5> sleeplocks:attempt(my_lock). 68 | {error,unavailable} 69 | 70 | % release the lock again 71 | 6> sleeplocks:release(my_lock). 72 | ok 73 | 74 | % handle acquisition and locking automatically 75 | 7> sleeplocks:execute(my_lock, fun() -> 76 | 7> 3 77 | 7> end). 78 | 3 79 | ``` 80 | 81 | ### Elixir 82 | 83 | ```elixir 84 | # create a new single lock (with a name) 85 | iex(1)> :sleeplocks.new(1, [ name: :my_lock ]) 86 | {:ok, #PID<0.179.0>} 87 | 88 | # take ownership of the lock 89 | iex(2)> :sleeplocks.acquire(:my_lock) 90 | :ok 91 | 92 | # release the current hold on a lock 93 | iex(3)> :sleeplocks.release(:my_lock) 94 | :ok 95 | 96 | # attempt to acquire a lock (which will succeed) 97 | iex(4)> :sleeplocks.attempt(:my_lock) 98 | :ok 99 | 100 | # now that it's taken, other attempts will fail 101 | iex(5)> :sleeplocks.attempt(:my_lock) 102 | {:error, :unavailable} 103 | 104 | # release the lock again 105 | iex(6)> :sleeplocks.release(:my_lock) 106 | :ok 107 | 108 | % handle acquisition and locking automatically 109 | iex(7)> :sleeplocks.execute(:my_lock, fn -> 110 | iex(7)> 3 111 | iex(7)> end) 112 | 3 113 | ``` 114 | 115 | ## Examples 116 | 117 | This example is in Elixir, but it should be fairly understandable for those coming from 118 | both languages. It simply spawns 6 processes which each attempt to hold a lock for 10 119 | seconds. As the lock is created with only 2 slots, this runs for 30 seconds and 2 of our 120 | spawned tasks can hold the lock at any given time. 121 | 122 | ```elixir 123 | # First create a new lock, with 2 slots only 124 | {:ok, ref} = :sleeplocks.new(2) 125 | 126 | # Then spawn 6 tasks, which each just sleep for 10 seconds 127 | # after acquiring the lock. This means that 2 processes will 128 | # acquire a lock and then release after 10 seconds. This 129 | # will repeat 3 times (6 / 2) until 30 seconds are up. 130 | for idx <- 1..6 do 131 | Task.start(fn -> 132 | :sleeplocks.execute(ref, fn -> 133 | IO.puts("Locked #{idx}") 134 | Process.sleep(10_000) 135 | IO.puts("Releasing #{idx}") 136 | end) 137 | end) 138 | end 139 | ``` 140 | 141 | -------------------------------------------------------------------------------- /src/sleeplocks.erl: -------------------------------------------------------------------------------- 1 | %% @doc 2 | %% BEAM friendly spinlocks for Elixir/Erlang. 3 | %% 4 | %% This module provides a very simple API for managing locks 5 | %% inside a BEAM instance. It's modeled on spinlocks, but works 6 | %% through message passing rather than loops. Locks can have 7 | %% multiple slots to enable arbitrary numbers of associated 8 | %% processes. The moment a slot is freed, the next awaiting 9 | %% process acquires the lock. 10 | %% 11 | %% All of this is done in a simple Erlang process so there's 12 | %% very little dependency, and management is extremely simple. 13 | -module(sleeplocks). 14 | -compile(inline). 15 | 16 | %% Public API 17 | -export([new/1, new/2, acquire/1, attempt/1, execute/2, release/1, start_link/1, start_link/2]). 18 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). 19 | 20 | %% Record definition for internal use. 21 | -record(lock, {slots, current=#{}, waiting=queue:new()}). 22 | 23 | %% Inlining for convenience functions. 24 | -compile({inline, [new/1, start_link/1, start_link/2]}). 25 | 26 | %% Name references available to call a lock. 27 | -type name() :: 28 | atom() | 29 | {local, Name :: atom()} | 30 | {global, GlobalName :: any()} | 31 | {via, Module :: atom(), ViaName :: any()}. 32 | 33 | %% Startup call return types to pass back through. 34 | -type start_ret() :: {ok, pid()} | ignore | {error, term()}. 35 | 36 | %% =================================================================== 37 | %% Public API 38 | %% =================================================================== 39 | 40 | %% @doc 41 | %% Creates a new lock with a provided concurrency factor. 42 | -spec new(Slots :: pos_integer()) -> start_ret(). 43 | new(Slots) -> 44 | new(Slots, []). 45 | 46 | %% @doc 47 | %% Creates a new lock with a provided concurrency factor. 48 | -spec new(Slots :: pos_integer(), Args :: list()) -> start_ret(). 49 | new(Slots, Args) when 50 | is_number(Slots), 51 | is_list(Args) 52 | -> 53 | case proplists:get_value(name, Args) of 54 | undefined -> 55 | gen_server:start_link(?MODULE, Slots, []); 56 | Name when is_atom(Name) -> 57 | gen_server:start_link({local, Name}, ?MODULE, Slots, []); 58 | Name -> 59 | gen_server:start_link(Name, ?MODULE, Slots, []) 60 | end. 61 | 62 | %% @doc 63 | %% Acquires a lock for the current process. 64 | %% 65 | %% This will block until a lock can be acquired. 66 | -spec acquire(Name :: name()) -> ok. 67 | acquire(Ref) -> 68 | gen_server:call(Ref, acquire, infinity). 69 | 70 | %% @doc 71 | %% Attempts to acquire a lock for the current process. 72 | %% 73 | %% In the case there are no slots available, an error will be 74 | %% returned immediately rather than waiting. 75 | -spec attempt(Name :: name()) -> ok | {error, unavailable}. 76 | attempt(Ref) -> 77 | gen_server:call(Ref, attempt). 78 | 79 | %% @doc 80 | %% Executes a function when a lock can be acquired. 81 | %% 82 | %% The lock is automatically released after the function has 83 | %% completed execution; there's no need to manually release. 84 | -spec execute(Name :: name(), Exec :: fun(() -> any())) -> ok. 85 | execute(Ref, Fun) -> 86 | acquire(Ref), 87 | try Fun() of 88 | Res -> Res 89 | after 90 | release(Ref) 91 | end. 92 | 93 | %% @doc 94 | %% Releases a lock held by the current process. 95 | -spec release(Name :: name()) -> ok. 96 | release(Ref) -> 97 | gen_server:call(Ref, release). 98 | 99 | %% @hidden 100 | %% Aliasing for Elixir interoperability. 101 | -spec start_link(Slots :: pos_integer()) -> start_ret(). 102 | start_link(Slots) -> 103 | new(Slots). 104 | 105 | %% @hidden 106 | %% Aliasing for Elixir interoperability. 107 | -spec start_link(Slots :: pos_integer(), Args :: list()) -> start_ret(). 108 | start_link(Slots, Args) -> 109 | new(Slots, Args). 110 | 111 | %%==================================================================== 112 | %% Callback functions 113 | %%==================================================================== 114 | 115 | %% @hidden 116 | %% Initialization phase. 117 | init(Slots) -> 118 | {ok, #lock{slots = Slots}}. 119 | 120 | %% @hidden 121 | %% Handles a lock acquisition (blocks until one is available). 122 | handle_call(acquire, Caller, #lock{waiting = Waiting} = Lock) -> 123 | case try_lock(Caller, Lock) of 124 | {ok, NewLock} -> 125 | {reply, ok, NewLock}; 126 | {error, unavailable} -> 127 | {noreply, Lock#lock{waiting = queue:snoc(Waiting, Caller)}} 128 | end; 129 | 130 | %% @hidden 131 | %% Handles an attempt to acquire a lock. 132 | handle_call(attempt, Caller, Lock) -> 133 | case try_lock(Caller, Lock) of 134 | {ok, NewLock} -> 135 | {reply, ok, NewLock}; 136 | {error, unavailable} = E -> 137 | {reply, E, Lock} 138 | end; 139 | 140 | %% @hidden 141 | %% Handles the release of a previously acquired lock. 142 | handle_call(release, {From, _Ref}, #lock{current = Current} = Lock) -> 143 | NewLock = case maps:is_key(From, Current) of 144 | false -> Lock; 145 | true -> 146 | NewCurrent = maps:remove(From, Current), 147 | next_caller(Lock#lock{current = NewCurrent}) 148 | end, 149 | {reply, ok, NewLock}. 150 | 151 | %% @hidden 152 | %% Empty shim to implement behaviour. 153 | handle_cast(_Msg, Lock) -> 154 | {noreply, Lock}. 155 | 156 | %% @hidden 157 | %% Empty shim to implement behaviour. 158 | handle_info(_Msg, Lock) -> 159 | {noreply, Lock}. 160 | 161 | %% @hidden 162 | %% Empty shim to implement behaviour. 163 | terminate(_Reason, _Lock) -> 164 | ok. 165 | 166 | %% @hidden 167 | %% Empty shim to implement behaviour. 168 | code_change(_Vsn, Lock, _Extra) -> 169 | {ok, Lock}. 170 | 171 | %%==================================================================== 172 | %% Private functions 173 | %%==================================================================== 174 | 175 | %% Locks a caller in the internal locks map. 176 | lock_caller({From, _Ref}, #lock{current = Current} = Lock) -> 177 | Lock#lock{current = maps:put(From, ok, Current)}. 178 | 179 | %% Attempts to pass a lock to a waiting caller. 180 | next_caller(#lock{waiting = Waiting} = Lock) -> 181 | case queue:out(Waiting) of 182 | {empty, {[], []}} -> 183 | Lock; 184 | {{value, Next}, NewWaiting} -> 185 | gen_server:reply(Next, ok), 186 | NewLock = lock_caller(Next, Lock), 187 | NewLock#lock{waiting = NewWaiting} 188 | end. 189 | 190 | %% Attempts to acquire a lock for a calling process 191 | try_lock(Caller, #lock{slots = Slots, current = Current} = Lock) -> 192 | case maps:size(Current) of 193 | S when S == Slots -> 194 | {error, unavailable}; 195 | _ -> 196 | {ok, lock_caller(Caller, Lock)} 197 | end. 198 | 199 | %% =================================================================== 200 | %% Private test cases 201 | %% =================================================================== 202 | 203 | -ifdef(TEST). 204 | -include_lib("eunit/include/eunit.hrl"). 205 | -endif. 206 | --------------------------------------------------------------------------------