├── .github └── workflows │ └── erlang.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── elvis.config ├── nextroll.dict ├── rebar.config ├── rebar.lock ├── src ├── spillway.app.src ├── spillway.erl ├── spillway_app.erl ├── spillway_srv.erl └── spillway_sup.erl └── test └── spillway_SUITE.erl /.github/workflows/erlang.yml: -------------------------------------------------------------------------------- 1 | name: Erlang CI 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | ERL_FLAGS: "-enable-feature all" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-24.04 11 | 12 | strategy: 13 | matrix: 14 | include: 15 | - otp: '25.2.1' 16 | rebar: '3.20.0' 17 | - otp: '27.3.4' 18 | rebar: '3.24.0' 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: erlef/setup-beam@v1 23 | id: setup-beam 24 | with: 25 | otp-version: ${{matrix.otp}} 26 | rebar3-version: ${{matrix.rebar}} 27 | - name: Restore _build 28 | uses: actions/cache@v4 29 | with: 30 | path: _build 31 | key: _build-cache-for-os-${{runner.os}}-otp-${{steps.setup-beam.outputs.otp-version}}-rebar3-${{steps.setup-beam.outputs.rebar3-version}}-hash-${{hashFiles('rebar.lock')}} 32 | - name: Restore rebar3's cache 33 | uses: actions/cache@v4 34 | with: 35 | path: ~/.cache/rebar3 36 | key: rebar3-cache-for-os-${{runner.os}}-otp-${{steps.setup-beam.outputs.otp-version}}-rebar3-${{steps.setup-beam.outputs.rebar3-version}}-hash-${{hashFiles('rebar.lock')}} 37 | - name: Compile 38 | run: rebar3 compile 39 | - name: Format check 40 | run: rebar3 format --verify 41 | - name: Run tests and verifications 42 | run: rebar3 test 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | doc/ 2 | .rebar3 3 | _* 4 | .eunit 5 | *.o 6 | *.beam 7 | *.plt 8 | *.swp 9 | *.swo 10 | .erlang.cookie 11 | ebin 12 | log 13 | erl_crash.dump 14 | .rebar 15 | logs 16 | _build 17 | .idea 18 | *.iml 19 | rebar3.crashdump 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | See the [Releases](../../releases) page. 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2018 AdRoll, Inc. Miriam Pena and Mike Watters 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REBAR = rebar3 2 | 3 | all: compile 4 | 5 | deps: 6 | @$(REBAR) unlock 7 | @$(REBAR) upgrade 8 | 9 | compile: 10 | @$(REBAR) compile 11 | 12 | edoc: 13 | @$(REBAR) doc 14 | 15 | test: compile lint dialyzer xref ct cover 16 | 17 | ct: 18 | ifdef SUITE 19 | @$(REBAR) ct --suite=$(SUITE) 20 | else 21 | @$(REBAR) ct 22 | endif 23 | 24 | clean: 25 | rm -rf _build 26 | 27 | dialyzer: 28 | @$(REBAR) dialyzer 29 | 30 | xref: 31 | @$(REBAR) xref 32 | 33 | cover: 34 | @$(REBAR) cover 35 | 36 | lint: 37 | @$(REBAR) lint 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Overview 2 | -------------- 3 | 4 | Spillway is an Erlang OTP application used for load shedding. The idea behind spillway is to use it 5 | to be able to limit the number of in flight concurrent calls to a section of code. 6 | 7 | Some examples: 8 | - As a server you can use spillway to limit the number of concurrent running requests to a service. You can 9 | determine when to throw away some requests by considering each request type and its weight. 10 | A request weight is measured by the cost fo performing the work (CPU/MEMORY) and the cost to not perform 11 | the work (BUSSINESS IMPACT). 12 | - As a client, when you face a failing server you might choose to retry a request. Spillway will let you 13 | implement a simple controlled-in-size-buffer mechanism that will allow you to retry some of the requests without 14 | running out of memory or resources. 15 | 16 | Example of use 17 | ---------------- 18 | 19 | A process about to execute a named section of code whose maximum parallelism 20 | should be limited will call `spillway:enter/2/3` with the name, the weight, and limit. 21 | 22 | If the return value is the 2-tuple `{true, TotalWeight}`, the process may enter the section of code 23 | (there now being TotalWeight in use concurrently-executing accesses), and otherwise not. 24 | 25 | If the process entered the section of code, it should call `spillway:leave/2` with the name and weight 26 | after completion. 27 | 28 | No special arrangement is made to handle process exits. If a process dies without 29 | calling `spillway:leave/1`, the counter will be inaccurate. This is intentional, 30 | and callers should make arrangements to mitigate this occurrence. 31 | 32 | ``` 33 | case spillway:enter(running_requests, Weight, Limit) of 34 | {true, Value} -> 35 | try 36 | continue_executing(Something); 37 | after 38 | spillway:leave(running_requests, Weight) 39 | end; 40 | false -> 41 | discard(Something) 42 | 43 | end. 44 | 45 | ``` 46 | 47 | Setup 48 | ------- 49 | 50 | - Add the application to your rebar3 dependencies and start the application normally. 51 | Alternatively you can also attach the supervision tree directly to the main supervisor of your 52 | application. 53 | 54 | Implementation 55 | ---------------- 56 | Spillway is implemented based on ETS-based bounded named counters. 57 | 58 | 59 | Build 60 | ----- 61 | 62 | ```shell 63 | make 64 | make ct 65 | ``` 66 | 67 | 1.x Changelog 68 | ------------- 69 | 1.1 2018-07-13 70 | * Remove the need to initialize counters 71 | 1.0.0 2018-07-11 72 | * Add initial implementation 73 | -------------------------------------------------------------------------------- /elvis.config: -------------------------------------------------------------------------------- 1 | [{elvis, 2 | [{config, 3 | [#{dirs => ["src"], 4 | filter => "*.erl", 5 | ruleset => erl_files, 6 | rules => [{elvis_style, state_record_and_type, disable}]}, 7 | #{dirs => ["test"], 8 | filter => "*.erl", 9 | ruleset => erl_files, 10 | rules => 11 | [%% Variables in eunit macros are called, for instance, __V 12 | {elvis_style, variable_naming_convention, #{regex => "^_?_?([A-Z][0-9a-zA-Z]*)_?$"}}, 13 | {elvis_style, nesting_level, disable}]}, 14 | #{dirs => ["."], 15 | filter => "*rebar.config", 16 | ruleset => rebar_config}, 17 | #{dirs => ["."], 18 | filter => "elvis.config", 19 | ruleset => elvis_config}]}]}]. 20 | -------------------------------------------------------------------------------- /nextroll.dict: -------------------------------------------------------------------------------- 1 | adroll 2 | github 3 | rebar3 4 | pena 5 | watters 6 | noninfringement 7 | api 8 | limit-reaching 9 | non-incrementing 10 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, 2 | [warn_unused_import, warn_export_vars, warnings_as_errors, verbose, report, debug_info]}. 3 | 4 | {minimum_otp_vsn, "25"}. 5 | 6 | {cover_enabled, true}. 7 | 8 | {cover_opts, [verbose]}. 9 | 10 | {deps, []}. 11 | 12 | {dialyzer, 13 | [{warnings, 14 | [unknown, no_return, unmatched_returns, error_handling, missing_return, extra_return]}, 15 | {plt_apps, top_level_deps}, 16 | {plt_extra_apps, []}, 17 | {plt_location, local}, 18 | {base_plt_apps, [erts, stdlib, kernel]}, 19 | {base_plt_location, global}]}. 20 | 21 | {xref_checks, 22 | [undefined_function_calls, 23 | locals_not_used, 24 | deprecated_function_calls, 25 | deprecated_functions]}. 26 | 27 | {spellcheck, 28 | [{ignore_regex, "(eunit|~>|--|==|\\^|_|//|\\d[.]\\d|[{].+[}]|[+])"}, 29 | {files, ["src/*", "test/*"]}, 30 | {additional_dictionaries, ["nextroll.dict"]}]}. 31 | 32 | {alias, [{test, [format, spellcheck, lint, hank, xref, dialyzer, ct, cover]}]}. 33 | 34 | {project_plugins, 35 | [{rebar3_hex, "~> 7.0.7"}, 36 | {rebar3_format, "~> 1.3.0"}, 37 | {rebar3_lint, "~> 3.2.3"}, 38 | {rebar3_hank, "~> 1.4.0"}, 39 | {rebar3_sheldon, "~> 0.4.3"}]}. 40 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | []. 2 | -------------------------------------------------------------------------------- /src/spillway.app.src: -------------------------------------------------------------------------------- 1 | {application, 2 | spillway, 3 | [{description, "A load shedding application"}, 4 | {vsn, "1.3.0"}, 5 | {registered, []}, 6 | {maintainers, ["AdRoll"]}, 7 | {links, [{"Github", "https://github.com/AdRoll/spillway"}]}, 8 | {mod, {spillway_app, []}}, 9 | {build_tools, ["rebar3"]}, 10 | {applications, [kernel, stdlib]}, 11 | {env, []}, 12 | {modules, []}, 13 | {maintainers, []}, 14 | {licenses, ["MIT"]}, 15 | {links, []}]}. 16 | -------------------------------------------------------------------------------- /src/spillway.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% The MIT License (MIT) 3 | %% Copyright (c) 2018 AdRoll, Inc. Miriam Pena and Mike Watters 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 | %% 23 | %%------------------------------------------------------------------- 24 | -module(spillway). 25 | 26 | -export([enter/2, enter/3, leave/1, leave/2, cur/1, state/0]). 27 | 28 | %%%=================================================================== 29 | %%% External functions 30 | %%%=================================================================== 31 | 32 | %% @doc Attempt to increment the named counter, respecting the given limit. If the counter was 33 | %% successfully incremented, return {true, NewValue}. Otherwise, return false. 34 | -spec enter(term(), non_neg_integer()) -> false | {true, non_neg_integer()}. 35 | enter(Name, Limit) -> 36 | enter(Name, 1, Limit). 37 | 38 | -spec enter(term(), non_neg_integer(), non_neg_integer()) -> 39 | false | {true, non_neg_integer()}. 40 | enter(Name, Size, Limit) when Size > 0 -> 41 | spillway_srv:enter(Name, Size, Limit). 42 | 43 | %% @doc Attempt to decrement the named counter, with a lower limit of 0. Return the new value. 44 | %% The counter must already exist. 45 | -spec leave(term()) -> non_neg_integer(). 46 | leave(Name) -> 47 | leave(Name, 1). 48 | 49 | -spec leave(term(), non_neg_integer()) -> non_neg_integer(). 50 | leave(Name, Size) -> 51 | spillway_srv:leave(Name, Size). 52 | 53 | %% @doc Return the current counter value. It will return 0 if it does not exist. 54 | -spec cur(term()) -> non_neg_integer(). 55 | cur(Name) -> 56 | spillway_srv:cur(Name). 57 | 58 | %% @doc For debug purposes. Returns the state of all counters. 59 | state() -> 60 | spillway_srv:state(). 61 | -------------------------------------------------------------------------------- /src/spillway_app.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% The MIT License (MIT) 3 | %% Copyright (c) 2018 AdRoll, Inc. Miriam Pena and Mike Watters 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 | %% 23 | %%------------------------------------------------------------------- 24 | -module(spillway_app). 25 | 26 | -behaviour(application). 27 | 28 | %% Application callbacks 29 | -export([start/2, stop/1]). 30 | 31 | %%==================================================================== 32 | %% API 33 | %%==================================================================== 34 | 35 | start(_StartType, _StartArgs) -> 36 | spillway_sup:start_link(). 37 | 38 | %%-------------------------------------------------------------------- 39 | stop(_State) -> 40 | ok. 41 | 42 | %%==================================================================== 43 | %% Internal functions 44 | %%==================================================================== 45 | -------------------------------------------------------------------------------- /src/spillway_srv.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% The MIT License (MIT) 3 | %% Copyright (c) 2018 AdRoll, Inc. Miriam Pena and Mike Watters 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 | %% 23 | %%------------------------------------------------------------------- 24 | -module(spillway_srv). 25 | 26 | -behaviour(gen_server). 27 | 28 | %% API 29 | -export([start_link/0, enter/3, leave/2, cur/1, state/0]). 30 | %% gen_server callbacks 31 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2]). 32 | 33 | -define(TID, spillway). 34 | 35 | -record(counter, {name, value = 0 :: non_neg_integer()}). 36 | 37 | -elvis([{elvis_style, dont_repeat_yourself, disable}]). 38 | 39 | %%%=================================================================== 40 | %%% External functions 41 | %%%=================================================================== 42 | 43 | %% Attempt to increment the named counter, respecting the given limit. If the counter was 44 | %% successfully incremented, return {true, NewValue}. Otherwise, return false. 45 | -spec enter(term(), non_neg_integer(), non_neg_integer()) -> 46 | false | {true, non_neg_integer()}. 47 | enter(Name, Size, Limit) when Size > 0 -> 48 | %% note: update_counter accepts a list of operations. we need to know whether 49 | %% we were the process to successfully increment a limit-reaching value, so we 50 | %% use an initial non-incrementing operation to read the existing value. if 51 | %% the result is [X, X+Size], we successfully incremented the counter. if we 52 | %% failed, the result will be [X, X]. 53 | [OldValue, NewValue] = 54 | ets:update_counter(?TID, 55 | Name, 56 | [{#counter.value, 0}, {#counter.value, Size, Limit, Limit}], 57 | #counter{name = Name}), 58 | Expected = OldValue + Size, 59 | case NewValue of 60 | OldValue -> 61 | %% We did not increment 62 | false; 63 | Expected -> 64 | %% We incremented 65 | {true, Expected}; 66 | Limit -> 67 | %% We incremented over the limit so limit is set 68 | {true, Limit} 69 | end. 70 | 71 | %% Attempt to decrement the named counter, with a lower limit of 0. Return the new value. 72 | %% The counter must already exist. 73 | -if(?OTP_RELEASE =< 25). 74 | 75 | -spec leave(term(), non_neg_integer()) -> non_neg_integer(). 76 | leave(Name, Size) -> 77 | ets:update_counter(?TID, Name, {#counter.value, -Size, 0, 0}). 78 | 79 | - else . 80 | 81 | -spec leave(term(), non_neg_integer()) -> non_neg_integer(). 82 | leave(Name, Size) -> 83 | case ets:update_counter(?TID, Name, {#counter.value, -Size, 0, 0}) of 84 | [] -> 85 | 0; 86 | [Result | _] -> 87 | Result; 88 | Result -> 89 | Result 90 | end. 91 | 92 | -endif. 93 | 94 | %% Return the current counter value. 95 | -spec cur(term()) -> non_neg_integer(). 96 | cur(Name) -> 97 | try 98 | ets:lookup_element(?TID, Name, #counter.value) 99 | catch 100 | error:badarg -> 101 | 0 102 | end. 103 | 104 | %% For debug purposes, return the state of all counters 105 | -spec state() -> [term()]. 106 | state() -> 107 | ets:tab2list(?TID). 108 | 109 | %%%=================================================================== 110 | %%% Internal functions 111 | %%%=================================================================== 112 | 113 | start_link() -> 114 | gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). 115 | 116 | init(_Args) -> 117 | _ = create_ets(), 118 | {ok, stateless}. 119 | 120 | handle_call(_Request, _From, State) -> 121 | Reply = {error, not_implemented}, 122 | {reply, Reply, State}. 123 | 124 | handle_cast(_Msg, State) -> 125 | {noreply, State}. 126 | 127 | handle_info(_Info, State) -> 128 | {noreply, State}. 129 | 130 | %%%% Internal Functions 131 | create_ets() -> 132 | ets:new(?TID, 133 | [set, 134 | named_table, 135 | public, 136 | {keypos, #counter.name}, 137 | {read_concurrency, true}, 138 | {write_concurrency, auto}, 139 | {decentralized_counters, true}]). 140 | -------------------------------------------------------------------------------- /src/spillway_sup.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% The MIT License (MIT) 3 | %% Copyright (c) 2018 AdRoll, Inc. Miriam Pena and Mike Watters 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 | %% 23 | %%------------------------------------------------------------------- 24 | -module(spillway_sup). 25 | 26 | -behaviour(supervisor). 27 | 28 | %% API 29 | -export([start_link/0]). 30 | %% Supervisor callbacks 31 | -export([init/1]). 32 | 33 | -define(SERVER, ?MODULE). 34 | 35 | %%==================================================================== 36 | %% API functions 37 | %%==================================================================== 38 | 39 | start_link() -> 40 | supervisor:start_link({local, ?SERVER}, ?MODULE, []). 41 | 42 | %%==================================================================== 43 | %% Supervisor callbacks 44 | %%==================================================================== 45 | 46 | init([]) -> 47 | Procs = [child(spillway_srv, worker, [])], 48 | {ok, {{one_for_all, 0, 1}, Procs}}. 49 | 50 | %%==================================================================== 51 | %% Internal functions 52 | %%==================================================================== 53 | 54 | child(I, Type, Args) -> 55 | {I, {I, start_link, Args}, permanent, 5000, Type, [I]}. 56 | -------------------------------------------------------------------------------- /test/spillway_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% The MIT License (MIT) 3 | %% Copyright (c) 2018 AdRoll, Inc. Miriam Pena and Mike Watters 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 | %% 23 | %%------------------------------------------------------------------- 24 | -module(spillway_SUITE). 25 | 26 | -include_lib("eunit/include/eunit.hrl"). 27 | 28 | -behaviour(ct_suite). 29 | 30 | -export([all/0, init_per_suite/1, end_per_suite/1, suite/0, init_per_testcase/2, 31 | end_per_testcase/2]). 32 | -export([complex/1, simple/1]). 33 | 34 | -define(TABLE, test_counter). 35 | 36 | %%%============================================================================= 37 | %%% common_test callbacks 38 | %%%============================================================================= 39 | 40 | all() -> 41 | [simple, complex]. 42 | 43 | suite() -> 44 | [{timetrap, {seconds, 15}}]. 45 | 46 | init_per_suite(Conf) -> 47 | Conf. 48 | 49 | end_per_suite(_Conf) -> 50 | ok. 51 | 52 | init_per_testcase(_Module, Conf) -> 53 | ok = application:start(spillway), 54 | Conf. 55 | 56 | end_per_testcase(_Module, _Conf) -> 57 | ok = application:stop(spillway), 58 | ok. 59 | 60 | %%%============================================================================= 61 | %%% Tests 62 | %%%============================================================================= 63 | 64 | simple(_) -> 65 | ?assertEqual(false, spillway:enter(?TABLE, 0)), 66 | ?assertEqual({true, 1}, spillway:enter(?TABLE, 2)), 67 | ?assertEqual({true, 2}, spillway:enter(?TABLE, 2)), 68 | ?assertEqual(false, spillway:enter(?TABLE, 2)), 69 | ?assertEqual(1, spillway:leave(?TABLE)), 70 | ?assertEqual(0, spillway:leave(?TABLE)), 71 | ?assertEqual(0, spillway:leave(?TABLE)), 72 | ok. 73 | 74 | spawn_proc(ProcN, Limit, SignalGo, SignalStop, Parent) -> 75 | spawn_monitor(fun() -> 76 | monitor(process, SignalGo), 77 | monitor(process, SignalStop), 78 | receive 79 | {'DOWN', _, process, SignalGo, _} -> 80 | case spillway:enter(?TABLE, ProcN, Limit) of 81 | {true, N} -> 82 | send_parent(Parent, {entered, self(), N}), 83 | receive 84 | {'DOWN', _, process, SignalStop, _} -> 85 | spillway:leave(?TABLE, ProcN) 86 | end; 87 | false -> 88 | send_parent(Parent, {not_entered, self()}), 89 | ok 90 | end 91 | end 92 | end). 93 | 94 | complex(_) -> 95 | NProcs = 2000, 96 | Limit = 140000, 97 | Parent = self(), 98 | SignalGo = signal(), 99 | SignalStop = signal(), 100 | Processes = 101 | [element(1, spawn_proc(ProcN, Limit, SignalGo, SignalStop, Parent)) 102 | || ProcN <- lists:seq(1, NProcs)], 103 | SignalGo ! go, 104 | 105 | %% Collect enter msg 106 | N = collect_values(Processes, 0), 107 | ?assert(N =< Limit), 108 | Value = spillway:cur(?TABLE), 109 | ?assert(Value =< Limit), 110 | 111 | %% wait for all the workers to leave and finish 112 | SignalStop ! go, 113 | wait_for_down(Processes), 114 | ?assertMatch(0, spillway:cur(?TABLE)), 115 | ok. 116 | 117 | send_parent(Parent, Msg) -> 118 | Parent ! Msg. 119 | 120 | collect_values([], Max) -> 121 | Max; 122 | collect_values(ProcessesSignal, Cur) -> 123 | receive 124 | {not_entered, Pid} -> 125 | collect_values(ProcessesSignal -- [Pid], Cur); 126 | {entered, Pid, M} when M > Cur -> 127 | collect_values(ProcessesSignal -- [Pid], M); 128 | {entered, Pid, _} -> 129 | collect_values(ProcessesSignal -- [Pid], Cur) 130 | end. 131 | 132 | wait_for_down([]) -> 133 | ok; 134 | wait_for_down(ProcessesExited) -> 135 | receive 136 | {'DOWN', _, process, Pid, _} -> 137 | wait_for_down(ProcessesExited -- [Pid]) 138 | end. 139 | 140 | signal() -> 141 | spawn(fun() -> 142 | receive 143 | go -> 144 | ok 145 | end 146 | end). 147 | --------------------------------------------------------------------------------