├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── erlang.mk ├── rebar.config ├── src ├── hottub.app.src ├── hottub.erl ├── ht_pool.erl ├── ht_sup.erl ├── ht_worker.erl └── ht_worker_sup.erl └── test ├── benchmark.erl ├── hottub_SUITE.erl └── test_worker.erl /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *.un~ 4 | *.swp 5 | *.pt.py 6 | data 7 | .coverage 8 | *.sqlite 9 | _build 10 | *.profile 11 | *~ 12 | ebin/* 13 | *.dump 14 | deps 15 | *.beam 16 | *.app 17 | *.eunit 18 | \#*\# 19 | rel/corbel 20 | *_data 21 | priv/machine.cfg 22 | logs 23 | log 24 | *.plt 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2015 Tom Burdick 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT = hottub 2 | CT_SUITES = hottub 3 | 4 | include erlang.mk 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Hot Tub 2 | ======= 3 | 4 | HotTub is a simple, fast, permanent erlang worker pool. 5 | 6 | Goals 7 | ----- 8 | 9 | * Keeps some number of worker processes alive at all times. 10 | * Add as little latency as possible to using a worker process under all 11 | circumstances. 12 | * Use only closures as the public interface to avoid missing worker processes 13 | due to errors or missing return to pool type calls. Closures ensure errors 14 | and returns are handled automatically for the callers. 15 | 16 | Primarily I have used this for database workers though other uses are clearly 17 | possible. When using this for connection pools the connections themselves should 18 | be lazy. Meaning that the process should not fail due to disconnects. 19 | 20 | Non-Goals 21 | --------- 22 | 23 | * Handle extraordinary large numbers of requests. In those cases several pools 24 | could be used in a rand() mod n_pools style distribution if desired. Or other 25 | libraries dealing with such situations might be a better choice. 26 | * Deal with the connection cycle of network client libraries, that should be 27 | done as something like a gen_fsm as a worker instead or a lazy retry loop 28 | in gen_server:init. 29 | 30 | Implementation 31 | --------------- 32 | 33 | HotTub uses a gen_server process to manage a queue of available workers and 34 | of requests for workers. When a worker is available it is dequeued and 35 | given away. If there are no workers then the request for a worker is queued. 36 | As soon as a worker is returned if a request is queued the worker is given away. 37 | 38 | A best effort at avoiding unnecessary work has been done however things could 39 | still probably be better. 40 | 41 | There is a benchmark as part of the test suite which can be run to give you an 42 | idea of the overhead of hottubs worker pool management routines. 43 | 44 | Several implementations were tried prior to settling on this one, including 45 | an ETS table with N workers using rand() mod n_workers selection, trying 46 | to find an available worker from there linearly. This was found to perform 47 | faster in some cases, but very poorly in the worst case. See the git history 48 | for more information. 49 | 50 | 51 | Example Usage 52 | ------------- 53 | 54 | demo_worker.erl 55 | 56 | ``` erlang 57 | -module(demo_worker). 58 | 59 | -export([start_link/0]). 60 | 61 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, 62 | terminate/2, code_change/3]). 63 | 64 | start_link() -> 65 | gen_server:start_link(?MODULE, [], []). 66 | 67 | init([]) -> 68 | {ok, undefined} 69 | 70 | handle_call({add, A, B}, _From, State) -> 71 | {reply, A+B, State}. 72 | 73 | handle_cast({print, Message}, State) -> 74 | io:format("~p~n", [Message]), 75 | {noreply, State}. 76 | 77 | handle_info(_Info, State) -> 78 | {noreply, State}. 79 | 80 | terminate(_Reason, State) -> 81 | ok. 82 | 83 | code_change(_OldVsn, State, _Extra) -> 84 | {ok, State}. 85 | ``` 86 | 87 | erl shell 88 | 89 | ``` erlang 90 | hottub:start_link(demo, 5, demo_worker, start_link, []). 91 | hottub:call(demo, {add, 5, 5}). 92 | hottub:call(demo, {add, 5, 5}, 1000). %% call with timeout 93 | hottub:cast(demo, {print, "what up from a pool"}). 94 | hottub:execute(demo, 95 | fun(Worker) -> 96 | io:format("causing a worker to crash~n"), 97 | gen_server:call(Worker, {hocus_pocus}) end). 98 | hottub:stop(demo). 99 | ``` 100 | -------------------------------------------------------------------------------- /erlang.mk: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013, Loïc Hoguin 2 | # 3 | # Permission to use, copy, modify, and/or distribute this software for any 4 | # purpose with or without fee is hereby granted, provided that the above 5 | # copyright notice and this permission notice appear in all copies. 6 | # 7 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | # Verbosity and tweaks. 16 | 17 | V ?= 0 18 | 19 | appsrc_verbose_0 = @echo " APP " $(PROJECT).app.src; 20 | appsrc_verbose = $(appsrc_verbose_$(V)) 21 | 22 | erlc_verbose_0 = @echo " ERLC " $(filter %.erl %.core,$(?F)); 23 | erlc_verbose = $(erlc_verbose_$(V)) 24 | 25 | xyrl_verbose_0 = @echo " XYRL " $(filter %.xrl %.yrl,$(?F)); 26 | xyrl_verbose = $(xyrl_verbose_$(V)) 27 | 28 | dtl_verbose_0 = @echo " DTL " $(filter %.dtl,$(?F)); 29 | dtl_verbose = $(dtl_verbose_$(V)) 30 | 31 | gen_verbose_0 = @echo " GEN " $@; 32 | gen_verbose = $(gen_verbose_$(V)) 33 | 34 | .PHONY: all clean-all app clean deps clean-deps docs clean-docs \ 35 | build-tests tests build-plt dialyze 36 | 37 | # Deps directory. 38 | 39 | DEPS_DIR ?= $(CURDIR)/deps 40 | export DEPS_DIR 41 | 42 | REBAR_DEPS_DIR = $(DEPS_DIR) 43 | export REBAR_DEPS_DIR 44 | 45 | ALL_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(DEPS)) 46 | ALL_TEST_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(TEST_DEPS)) 47 | 48 | # Application. 49 | 50 | ERLC_OPTS ?= -Werror +debug_info +warn_export_all +warn_export_vars \ 51 | +warn_shadow_vars +warn_obsolete_guard # +bin_opt_info +warn_missing_spec 52 | COMPILE_FIRST ?= 53 | COMPILE_FIRST_PATHS = $(addprefix src/,$(addsuffix .erl,$(COMPILE_FIRST))) 54 | 55 | all: deps app 56 | 57 | clean-all: clean clean-deps clean-docs 58 | $(gen_verbose) rm -rf .$(PROJECT).plt $(DEPS_DIR) logs 59 | 60 | app: ebin/$(PROJECT).app 61 | $(eval MODULES := $(shell find ebin -name \*.beam \ 62 | | sed 's/ebin\///;s/\.beam/,/' | sed '$$s/.$$//')) 63 | $(appsrc_verbose) cat src/$(PROJECT).app.src \ 64 | | sed 's/{modules, \[\]}/{modules, \[$(MODULES)\]}/' \ 65 | > ebin/$(PROJECT).app 66 | 67 | define compile_erl 68 | $(erlc_verbose) ERL_LIBS=$(DEPS_DIR) erlc -v $(ERLC_OPTS) -o ebin/ \ 69 | -pa ebin/ -I include/ $(COMPILE_FIRST_PATHS) $(1) 70 | endef 71 | 72 | define compile_xyrl 73 | $(xyrl_verbose) erlc -v -o ebin/ $(1) 74 | $(xyrl_verbose) erlc $(ERLC_OPTS) -o ebin/ ebin/*.erl 75 | @rm ebin/*.erl 76 | endef 77 | 78 | define compile_dtl 79 | $(dtl_verbose) erl -noshell -pa ebin/ deps/erlydtl/ebin/ -eval ' \ 80 | Compile = fun(F) -> \ 81 | Module = list_to_atom( \ 82 | string:to_lower(filename:basename(F, ".dtl")) ++ "_dtl"), \ 83 | erlydtl_compiler:compile(F, Module, [{out_dir, "ebin/"}]) \ 84 | end, \ 85 | _ = [Compile(F) || F <- string:tokens("$(1)", " ")], \ 86 | init:stop()' 87 | endef 88 | 89 | ebin/$(PROJECT).app: src/*.erl $(wildcard src/*.core) \ 90 | $(wildcard src/*.xrl) $(wildcard src/*.yrl) \ 91 | $(wildcard templates/*.dtl) 92 | @mkdir -p ebin/ 93 | $(if $(strip $(filter %.erl %.core,$?)), \ 94 | $(call compile_erl,$(filter %.erl %.core,$?))) 95 | $(if $(strip $(filter %.xrl %.yrl,$?)), \ 96 | $(call compile_xyrl,$(filter %.xrl %.yrl,$?))) 97 | $(if $(strip $(filter %.dtl,$?)), \ 98 | $(call compile_dtl,$(filter %.dtl,$?))) 99 | 100 | clean: 101 | $(gen_verbose) rm -rf ebin/ test/*.beam erl_crash.dump 102 | 103 | # Dependencies. 104 | 105 | define get_dep 106 | @mkdir -p $(DEPS_DIR) 107 | git clone -n -- $(word 1,$(dep_$(1))) $(DEPS_DIR)/$(1) 108 | cd $(DEPS_DIR)/$(1) ; git checkout -q $(word 2,$(dep_$(1))) 109 | endef 110 | 111 | define dep_target 112 | $(DEPS_DIR)/$(1): 113 | $(call get_dep,$(1)) 114 | endef 115 | 116 | $(foreach dep,$(DEPS),$(eval $(call dep_target,$(dep)))) 117 | 118 | deps: $(ALL_DEPS_DIRS) 119 | @for dep in $(ALL_DEPS_DIRS) ; do $(MAKE) -C $$dep; done 120 | 121 | clean-deps: 122 | @for dep in $(ALL_DEPS_DIRS) ; do $(MAKE) -C $$dep clean; done 123 | 124 | # Documentation. 125 | 126 | docs: clean-docs 127 | $(gen_verbose) erl -noshell \ 128 | -eval 'edoc:application($(PROJECT), ".", []), init:stop().' 129 | 130 | clean-docs: 131 | $(gen_verbose) rm -f doc/*.css doc/*.html doc/*.png doc/edoc-info 132 | 133 | # Tests. 134 | 135 | $(foreach dep,$(TEST_DEPS),$(eval $(call dep_target,$(dep)))) 136 | 137 | build-test-deps: $(ALL_TEST_DEPS_DIRS) 138 | @for dep in $(ALL_TEST_DEPS_DIRS) ; do $(MAKE) -C $$dep; done 139 | 140 | build-tests: build-test-deps 141 | $(gen_verbose) ERL_LIBS=deps erlc -v $(ERLC_OPTS) -o test/ \ 142 | $(wildcard test/*.erl test/*/*.erl) -pa ebin/ 143 | 144 | CT_RUN = ct_run \ 145 | -no_auto_compile \ 146 | -noshell \ 147 | -pa ebin $(DEPS_DIR)/*/ebin \ 148 | -dir test \ 149 | -logdir logs 150 | # -cover test/cover.spec 151 | 152 | CT_SUITES ?= 153 | CT_SUITES_FULL = $(addsuffix _SUITE,$(CT_SUITES)) 154 | 155 | tests: ERLC_OPTS += -DTEST=1 +'{parse_transform, eunit_autoexport}' 156 | tests: clean deps app build-tests 157 | @mkdir -p logs/ 158 | @$(CT_RUN) -suite $(CT_SUITES_FULL) 159 | $(gen_verbose) rm -f test/*.beam 160 | 161 | # Dialyzer. 162 | 163 | PLT_APPS ?= 164 | DIALYZER_OPTS ?= -Werror_handling -Wrace_conditions \ 165 | -Wunmatched_returns # -Wunderspecs 166 | 167 | build-plt: deps app 168 | @dialyzer --build_plt --output_plt .$(PROJECT).plt \ 169 | --apps erts kernel stdlib $(PLT_APPS) $(ALL_DEPS_DIRS) 170 | 171 | dialyze: 172 | @dialyzer --src src --plt .$(PROJECT).plt --no_native $(DIALYZER_OPTS) 173 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [fail_on_warning, debug_info]}. 2 | 3 | {cover_enabled, true}. 4 | -------------------------------------------------------------------------------- /src/hottub.app.src: -------------------------------------------------------------------------------- 1 | {application, hottub, 2 | [ 3 | {description, "Permanent worker pool using supervisors"}, 4 | {vsn, "1.2.0"}, 5 | {registered, []}, 6 | {applications, [ 7 | kernel, 8 | stdlib 9 | ]}, 10 | {env, []} 11 | ]}. 12 | -------------------------------------------------------------------------------- /src/hottub.erl: -------------------------------------------------------------------------------- 1 | %% @author Tom Burdick 2 | %% @copyright 2011 Tom Burdick 3 | %% @doc Hottub API 4 | 5 | -module(hottub). 6 | 7 | %% api 8 | -export([start_link/5, stop/1, execute/2, execute/3, call/2, call/3, cast/2]). 9 | 10 | 11 | %% ---------------------------------------------------------------------------- 12 | %% api 13 | %% ---------------------------------------------------------------------------- 14 | 15 | %% @doc Start a linked hottub worker pool supervisor. 16 | -spec start_link(atom(), pos_integer(), atom(), atom(), list(any())) -> 17 | ignore | {error, any()} | {ok, pid()}. 18 | start_link(PoolName, Limit, Module, Function, Args) -> 19 | ht_sup:start_link(PoolName, Limit, Module, Function, Args). 20 | 21 | %% @doc Stop a hottub worker pool supervisor 22 | -spec stop(atom()) -> ok. 23 | stop(PoolName) -> 24 | ht_sup:stop(PoolName). 25 | 26 | %% @doc Perform a gen_server:call with a worker process. 27 | -spec call(atom(), any()) -> any(). 28 | call(PoolName, Args) -> 29 | execute(PoolName, 30 | fun(Worker) -> 31 | gen_server:call(Worker, Args) 32 | end). 33 | 34 | %% @doc Perform a gen_server:call with a worker process. 35 | -spec call(atom(), any(), timeout()) -> any(). 36 | call(PoolName, Args, Timeout) -> 37 | execute(PoolName, Timeout, 38 | fun(Worker) -> 39 | gen_server:call(Worker, Args) 40 | end). 41 | 42 | %% @doc Perform a gen_server:cast with a worker process. 43 | -spec cast(atom(), any()) -> any(). 44 | cast(PoolName, Args) -> 45 | execute(PoolName, 46 | fun(Worker) -> 47 | gen_server:cast(Worker, Args) 48 | end). 49 | 50 | %% @doc Execute a function using a worker waiting forever for a worker. 51 | -spec execute(atom(), fun((pid()) -> any())) -> any(). 52 | execute(PoolName, Function) -> 53 | execute(PoolName, infinity, Function). 54 | 55 | %% @doc Execute a function using a worker with a timeout for obtaining a worker 56 | -spec execute(atom(), timeout(), fun((pid()) -> any())) -> any(). 57 | execute(PoolName, Timeout, Function) -> 58 | Worker = ht_pool:checkout_worker(PoolName, Timeout), 59 | try 60 | Function(Worker) 61 | after 62 | ht_pool:checkin_worker(PoolName, Worker) 63 | end. 64 | -------------------------------------------------------------------------------- /src/ht_pool.erl: -------------------------------------------------------------------------------- 1 | %% @author Tom Burdick 2 | %% @copyright 2011 Tom Burdick 3 | %% @doc Hot Tub Pool Manager. 4 | %% Keeps two simple queues. One of workers and another of pending checkouts 5 | %% if there are no available workers. On checkin or addition of a worker 6 | %% if pending checkouts are waiting the worker is immediately handed off 7 | %% with little cost it is otherwise added back in to the queue of unused 8 | %% workers. 9 | %% 10 | %% A best attempt is made to ensure a worker is alive when checked out though 11 | %% in some circumstances a process may be dead on arrival if the last request 12 | %% caused it to have a delayed termination. Such scenarios are easily 13 | %% possibly when gen_server:cast or something equivalent. 14 | %% 15 | %% checkin/checkout should not be used directly. Instead the simple, 16 | %% functional wrapper hottub:execute should be used or one of its 17 | %% additional halpers hottub:call or hottub:cast. 18 | %% 19 | %% @end 20 | 21 | -module(ht_pool). 22 | 23 | -behaviour(gen_server). 24 | 25 | %% api 26 | -export([start_link/1]). 27 | -export([add_worker/2]). 28 | -export([checkout_worker/1]). 29 | -export([checkout_worker/2]). 30 | -export([checkin_worker/2]). 31 | 32 | %% gen_server callbacks 33 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, 34 | code_change/3]). 35 | 36 | -record(state, {poolname=undefined, unused=queue:new(), checkouts=queue:new()}). 37 | 38 | 39 | %% ---------------------------------------------------------------------------- 40 | %% api 41 | %% ---------------------------------------------------------------------------- 42 | 43 | %% @doc Start a linked pool manager. 44 | -spec start_link(atom()) -> {ok, pid()}. 45 | start_link(PoolName) -> 46 | gen_server:start_link({local, PoolName}, ?MODULE, [PoolName], []). 47 | 48 | %% @doc Called by ht_worker after the worker process has started. 49 | -spec add_worker(atom(), pid()) -> term(). 50 | add_worker(PoolName, Pid) -> 51 | gen_server:cast(PoolName, {add_worker, Pid}). 52 | 53 | %% @doc Checkin a worker. 54 | -spec checkin_worker(atom(), pid()) -> term(). 55 | checkin_worker(PoolName, Pid) when is_pid(Pid) -> 56 | %% only checkin live workers 57 | case is_process_alive(Pid) of 58 | true -> 59 | gen_server:cast(PoolName, {checkin_worker, Pid}); 60 | false -> 61 | ok 62 | end. 63 | 64 | %% @doc Checkout a worker. 65 | -spec checkout_worker(atom()) -> pid() | undefined. 66 | checkout_worker(PoolName) -> 67 | checkout_worker(PoolName, infinity). 68 | 69 | %% @doc Checkout a worker with a timeout 70 | -spec checkout_worker(atom(), timeout()) -> pid() | undefined. 71 | checkout_worker(PoolName, Timeout) -> 72 | Worker = gen_server:call(PoolName, {checkout_worker}, Timeout), 73 | %% only checkout live workers 74 | case is_process_alive(Worker) of 75 | true -> 76 | Worker; 77 | false -> 78 | checkout_worker(PoolName, Timeout) 79 | end. 80 | 81 | %% ------------------------------------------------------------------ 82 | %% gen_server callbacks 83 | %% ------------------------------------------------------------------ 84 | 85 | %% @private 86 | init([PoolName]) -> 87 | {ok, #state{poolname=PoolName}}. 88 | 89 | %% @private 90 | handle_call({checkout_worker}, From, State) -> 91 | case queue:out(State#state.unused) of 92 | {{value, Worker}, Unused} -> 93 | {reply, Worker, State#state{unused=Unused}}; 94 | {empty, _Unused} -> 95 | Checkouts = queue:in(From, State#state.checkouts), 96 | {noreply, State#state{checkouts=Checkouts}} 97 | end. 98 | 99 | %% @private 100 | handle_cast({checkin_worker, Worker}, State) -> 101 | case queue:out(State#state.checkouts) of 102 | {{value, P}, Checkouts} -> 103 | gen_server:reply(P, Worker), 104 | {noreply, State#state{checkouts=Checkouts}}; 105 | {empty, _Checkouts} -> 106 | Unused = queue:in(Worker, State#state.unused), 107 | {noreply, State#state{unused=Unused}} 108 | end; 109 | handle_cast({add_worker, Worker}, State) -> 110 | erlang:monitor(process, Worker), 111 | case queue:out(State#state.checkouts) of 112 | {{value, P}, Checkouts} -> 113 | gen_server:reply(P, Worker), 114 | {noreply, State#state{checkouts=Checkouts}}; 115 | {empty, _Checkouts} -> 116 | Unused = queue:in(Worker, State#state.unused), 117 | {noreply, State#state{unused=Unused}} 118 | end. 119 | 120 | %% @private 121 | handle_info({'DOWN', _, _, Worker, _}, State) -> 122 | Unused = queue:from_list(lists:delete(Worker, 123 | queue:to_list(State#state.unused))), 124 | {noreply, State#state{unused=Unused}}. 125 | 126 | %% @private 127 | terminate(_Reason, _State) -> 128 | ok. 129 | 130 | %% @private 131 | code_change(_OldVsn, State, _Extra) -> 132 | {ok, State}. 133 | -------------------------------------------------------------------------------- /src/ht_sup.erl: -------------------------------------------------------------------------------- 1 | %% @author Tom Burdick 2 | %% @copyright 2011 Tom Burdick. 3 | %% @doc HotTub Supervisor. 4 | 5 | -module(ht_sup). 6 | 7 | -behaviour(supervisor). 8 | 9 | %% api 10 | -export([start_link/5, stop/1]). 11 | 12 | %% supervisor callbacks 13 | -export([init/1]). 14 | 15 | 16 | %% ---------------------------------------------------------------------------- 17 | %% api 18 | %% ---------------------------------------------------------------------------- 19 | 20 | %% @doc Start linked hottub supervisor. 21 | -spec start_link(atom(), pos_integer(), atom(), atom(), list(any())) -> 22 | ignore | {error, term()} | {ok, pid()}. 23 | start_link(PoolName, Limit, Module, Function, Arguments) when 24 | is_atom(PoolName), 25 | is_integer(Limit), 26 | is_atom(Module), 27 | is_atom(Function), 28 | is_list(Arguments) -> 29 | supervisor:start_link({local, sup_name(PoolName)}, ?MODULE, 30 | [PoolName, Limit, Module, Function, Arguments]). 31 | 32 | %% @doc Stop a hottub supervisor. 33 | -spec stop(atom()) -> ok. 34 | stop(PoolName) -> 35 | SupName = sup_name(PoolName), 36 | case whereis(SupName) of 37 | undefined -> 38 | ok; 39 | Pid -> 40 | unlink(Pid), 41 | Ref = monitor(process, Pid), 42 | exit(Pid, shutdown), 43 | receive 44 | {'DOWN', Ref, process, Pid, shutdown} -> 45 | ok 46 | after 47 | 1000 -> 48 | erlang:exit(Pid, kill), 49 | ok 50 | end 51 | end. 52 | 53 | %% ---------------------------------------------------------------------------- 54 | %% private api 55 | %% ---------------------------------------------------------------------------- 56 | 57 | sup_name(PoolName) -> 58 | list_to_atom(atom_to_list(PoolName) ++ "_sup"). 59 | 60 | worker_sup_name(PoolName) -> 61 | atom_to_list(PoolName) ++ "_worker_sup". 62 | 63 | pool_name(PoolName) -> 64 | atom_to_list(PoolName) ++ "_pool". 65 | 66 | 67 | %% ---------------------------------------------------------------------------- 68 | %% supervisor callbacks 69 | %% ---------------------------------------------------------------------------- 70 | 71 | %% @private 72 | init([PoolName, Limit, Module, Function, Arguments]) -> 73 | PoolSpec = {pool_name(PoolName), 74 | {ht_pool, start_link, [PoolName]}, 75 | permanent, 2000, worker, [ht_pool]}, 76 | WorkerSupSpec = {worker_sup_name(PoolName), 77 | {ht_worker_sup, start_link, [PoolName, Limit, Module, Function, 78 | Arguments]}, 79 | permanent, 2000, supervisor, [ht_worker_sup]}, 80 | {ok, {{one_for_one, 5, 10}, [PoolSpec, WorkerSupSpec]}}. 81 | -------------------------------------------------------------------------------- /src/ht_worker.erl: -------------------------------------------------------------------------------- 1 | %% @author Tom Burdick 2 | %% @copyright 2011 Tom Burdick. 3 | %% @doc Hot Tub Worker. 4 | 5 | -module(ht_worker). 6 | 7 | -export([start_worker/2]). 8 | 9 | %% @doc The pool manager needs to know when a worker is alive. 10 | %% It turns out the simplest way is to simply wrap the function the 11 | %% supervisor calls to start a process with another that does some additional 12 | %% work. 13 | %% @end 14 | -spec start_worker(atom(), {atom(), atom(), list(any())}) 15 | -> {ok, pid()}. 16 | start_worker(PoolName, {Module, Function, Arguments}) -> 17 | {ok, Pid} = erlang:apply(Module, Function, Arguments), 18 | ht_pool:add_worker(PoolName, Pid), 19 | {ok, Pid}. 20 | -------------------------------------------------------------------------------- /src/ht_worker_sup.erl: -------------------------------------------------------------------------------- 1 | %% @author Tom Burdick 2 | %% @copyright 2011 Tom Burdick. 3 | %% @doc Hot Tub Worker Supervisor. 4 | 5 | -module(ht_worker_sup). 6 | 7 | -behaviour(supervisor). 8 | 9 | %% api 10 | -export([start_link/5]). 11 | 12 | %% supervisor callbacks 13 | -export([init/1]). 14 | 15 | 16 | %% ---------------------------------------------------------------------------- 17 | %% api 18 | %% ---------------------------------------------------------------------------- 19 | 20 | %% @doc Start linked hot tub worker supervisor. 21 | -spec start_link(atom(), pos_integer(), atom(), atom(), list(any())) -> 22 | {ok, pid()}. 23 | start_link(PoolName, Limit, Module, Function, Arguments) -> 24 | supervisor:start_link({local, sup_name(PoolName)}, ?MODULE, 25 | [PoolName, Limit, Module, Function, Arguments]). 26 | 27 | 28 | %% ---------------------------------------------------------------------------- 29 | %% private api 30 | %% ---------------------------------------------------------------------------- 31 | 32 | sup_name(PoolName) -> 33 | list_to_atom(atom_to_list(PoolName) ++ "_worker_sup"). 34 | 35 | worker_name(PoolName, Id) -> 36 | lists:flatten([atom_to_list(PoolName) | ["_worker_" 37 | | io_lib:format("~p", [Id])]]). 38 | 39 | 40 | %% ---------------------------------------------------------------------------- 41 | %% supervisor callbacks 42 | %% ---------------------------------------------------------------------------- 43 | 44 | %% @private 45 | init([PoolName, Limit, M, F, A]) -> 46 | ChildSpecs = lists:map( 47 | fun (Id) -> 48 | {worker_name(PoolName, Id), 49 | {ht_worker, start_worker, [PoolName, {M, F, A}]}, 50 | permanent, 2000, worker, [ht_worker, M]} 51 | end, lists:seq(0, Limit-1)), 52 | {ok, {{one_for_one, 10, 60}, ChildSpecs}}. 53 | -------------------------------------------------------------------------------- /test/benchmark.erl: -------------------------------------------------------------------------------- 1 | %% @author Tom Burdick 2 | %% @copyright 2011 Tom Burdick 3 | %% @doc Hottub Pool Benchmark Worker. 4 | 5 | -module(benchmark). 6 | 7 | -behaviour(gen_server). 8 | 9 | %% api 10 | -export([start_link/1, perform/3, results/1, stop/1]). 11 | 12 | %% gen_server callbacks 13 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). 14 | 15 | -record(state, {id=undefined, min=0, max=0, avg=0}). 16 | 17 | 18 | %% ---------------------------------------------------------------------------- 19 | %% api 20 | %% ---------------------------------------------------------------------------- 21 | 22 | %% @doc Start a linked test worker. 23 | -spec start_link(Id::any()) -> {ok, pid()}. 24 | start_link(Id) -> 25 | gen_server:start_link(?MODULE, [Id], []). 26 | 27 | %% @doc Perform a function many times recording the call time in an ets table. 28 | -spec perform(Pid::pid(), Function::fun(), Times::pos_integer()) -> ok. 29 | perform(Pid, Function, Times) -> 30 | gen_server:cast(Pid, {perform, Function, Times}). 31 | 32 | %% @doc Wait until the server is done working then return the min, max, and average call time. 33 | -spec results(Pid::pid()) -> {float(), float(), float()}. 34 | results(Pid) -> 35 | gen_server:call(Pid, {results}, infinity). 36 | 37 | %% @doc Stop a benchmark worker. 38 | -spec stop(Pid::pid()) -> any(). 39 | stop(Pid) -> 40 | gen_server:cast(Pid, {stop}). 41 | 42 | 43 | %% ------------------------------------------------------------------ 44 | %% gen_server callbacks 45 | %% ------------------------------------------------------------------ 46 | 47 | %% @private 48 | init([Id]) -> 49 | {ok, #state{id=Id}}. 50 | 51 | %% @private 52 | handle_call({results}, _From, State) -> 53 | {reply, {State#state.min, State#state.max, State#state.avg}, State}; 54 | handle_call(_Request, _From, State) -> 55 | {reply, ok, State}. 56 | 57 | %% @private 58 | handle_cast({perform, Function, Times}, State) -> 59 | {Min, Max, Sum} = 60 | lists:foldl( 61 | fun(_, {Min, Max, Sum}) -> 62 | Begin = erlang:now(), 63 | Function(), 64 | End = erlang:now(), 65 | Tdiff = timer:now_diff(End, Begin)*0.001, 66 | {min(Min, Tdiff), max(Max, Tdiff), Sum+Tdiff} 67 | end, 68 | {1000000000, 0, 0}, 69 | lists:seq(0, Times-1)), 70 | Mean = Sum/Times, 71 | {noreply, State#state{min=Min, max=Max, avg=Mean}}; 72 | handle_cast({stop}, State) -> 73 | {stop, normal, State}. 74 | 75 | %% @private 76 | handle_info(_Info, State) -> 77 | {noreply, State}. 78 | 79 | %% @private 80 | terminate(_Reason, _State) -> 81 | ok. 82 | 83 | %% @private 84 | code_change(_OldVsn, State, _Extra) -> 85 | {ok, State}. 86 | -------------------------------------------------------------------------------- /test/hottub_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2011-2013, Tom Burdick 2 | %% 3 | %% Permission to use, copy, modify, and/or distribute this software for any 4 | %% purpose with or without fee is hereby granted, provided that the above 5 | %% copyright notice and this permission notice appear in all copies. 6 | %% 7 | %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | -module(hottub_SUITE). 16 | 17 | -include_lib("common_test/include/ct.hrl"). 18 | 19 | %% ct. 20 | -export([all/0]). 21 | 22 | %% Tests. 23 | -export([start_stop/1]). 24 | -export([dead_worker/1]). 25 | -export([timeout/1]). 26 | -export([crash/1]). 27 | -export([benchmark/1]). 28 | 29 | all() -> 30 | [ 31 | start_stop, 32 | dead_worker, 33 | timeout, 34 | crash, 35 | benchmark 36 | ]. 37 | 38 | %% Basic Worker Pool Test. 39 | start_stop(_Config) -> 40 | {ok, Pid} = hottub:start_link(ss_pool, 1, test_worker, start_link, []), 41 | ok = hottub:stop(ss_pool), 42 | false = is_process_alive(Pid), 43 | ok. 44 | 45 | dead_worker(_Config) -> 46 | hottub:start_link(dead_pool, 1, test_worker, start_link, []), 47 | Pid = self(), 48 | BlockFun = fun() -> 49 | hottub:execute(dead_pool, fun(Worker) -> 50 | Pid ! waiting, 51 | receive 52 | continue -> 53 | test_worker:crash(Worker) 54 | end 55 | end) 56 | end, 57 | BlockedFun = fun() -> 58 | Pid ! waiting, 59 | hottub:execute(dead_pool, fun(Worker) -> 60 | case is_process_alive(Worker) of 61 | true -> 62 | Pid ! ok; 63 | false -> 64 | Pid ! fail 65 | end 66 | end) 67 | end, 68 | Blocker = spawn(BlockFun), 69 | receive 70 | waiting -> 71 | ok 72 | end, 73 | spawn(BlockedFun), 74 | receive 75 | waiting -> 76 | ok 77 | end, 78 | Blocker ! continue, 79 | receive 80 | ok -> 81 | ok; 82 | fail -> 83 | throw(fail) 84 | end. 85 | 86 | timeout(_Config) -> 87 | hottub:start_link(test_pool, 1, test_worker, start_link, []), 88 | hottub:execute(test_pool, 89 | fun(Worker) -> 90 | true = is_pid(Worker), 91 | try 92 | hottub:execute(test_pool, 100, fun(Worker0) -> 93 | false = is_pid(Worker0) 94 | end) 95 | catch 96 | exit:{timeout, _} -> 97 | ok 98 | end 99 | end), 100 | hottub:stop(test_pool), 101 | ok. 102 | 103 | 104 | crash(_Config) -> 105 | hottub:start_link(test_pool, 1, test_worker, start_link, []), 106 | hottub:execute(test_pool, 107 | fun(Worker) -> 108 | true = is_pid(Worker), 109 | test_worker:crash(Worker) 110 | end), 111 | hottub:execute(test_pool, 112 | fun(Worker) -> 113 | true = is_pid(Worker) 114 | end), 115 | hottub:stop(test_pool), 116 | ok. 117 | 118 | benchmark(_Config) -> 119 | NWorkers = 500, 120 | hottub:start_link(bench_pool, 100, test_worker, start_link, []), 121 | BenchFun = fun() -> 122 | hottub:execute(bench_pool, 123 | fun(Worker) -> 124 | test_worker:nothing(Worker) 125 | end) 126 | end, 127 | BenchWorkers = lists:map( 128 | fun(Id) -> 129 | {ok, Pid} = benchmark:start_link(Id), 130 | benchmark:perform(Pid, BenchFun, 1000), 131 | Pid 132 | end, lists:seq(0, NWorkers)), 133 | {Min, Max, AvgSum} = lists:foldl( 134 | fun(Pid, {Min, Max, AvgSum}) -> 135 | {RMin, RMax, RAvg} = benchmark:results(Pid), 136 | {min(RMin, Min), max(RMax, Max), AvgSum + RAvg} 137 | end, {10000000000, 0, 0}, BenchWorkers), 138 | Mean = AvgSum/NWorkers, 139 | hottub:stop(bench_pool), 140 | io:format(user, "Worker Execute Results: Min ~pms, Max ~pms, Mean ~pms~n", [Min, Max, Mean]), 141 | ok. 142 | -------------------------------------------------------------------------------- /test/test_worker.erl: -------------------------------------------------------------------------------- 1 | %% @author Tom Burdick 2 | %% @copyright 2011 Tom Burdick 3 | %% @doc Hot Tub Pool Test Worker. 4 | 5 | -module(test_worker). 6 | 7 | -behaviour(gen_server). 8 | 9 | %% api 10 | -export([start_link/0, nothing/1, crash/1, increment/1, count/1, stop/1]). 11 | 12 | %% gen_server callbacks 13 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). 14 | 15 | -record(state, {count=0}). 16 | 17 | 18 | %% ---------------------------------------------------------------------------- 19 | %% api 20 | %% ---------------------------------------------------------------------------- 21 | 22 | %% @doc Start a linked test worker. 23 | -spec start_link() -> {ok, pid()}. 24 | start_link() -> 25 | gen_server:start_link(?MODULE, [], []). 26 | 27 | %% @doc Do nothing. 28 | -spec nothing(Pid::pid()) -> ok. 29 | nothing(_) -> 30 | ok. 31 | 32 | %% @doc Increment counter. 33 | -spec increment(Pid::pid()) -> any(). 34 | increment(Pid) -> 35 | gen_server:call(Pid, {increment}). 36 | 37 | %% @doc Return counter. 38 | -spec count(Pid::pid()) -> any(). 39 | count(Pid) -> 40 | gen_server:call(Pid, {count}). 41 | 42 | %% @doc Crash a test worker. 43 | -spec crash(Pid::pid()) -> any(). 44 | crash(Pid) -> 45 | gen_server:cast(Pid, {crash}). 46 | 47 | %% @doc Stop a test worker. 48 | -spec stop(Pid::pid()) -> any(). 49 | stop(Pid) -> 50 | gen_server:cast(Pid, {stop}). 51 | 52 | 53 | %% ------------------------------------------------------------------ 54 | %% gen_server callbacks 55 | %% ------------------------------------------------------------------ 56 | 57 | %% @private 58 | init([]) -> 59 | {ok, #state{}}. 60 | 61 | %% @private 62 | handle_call({increment}, _From, State) -> 63 | C = State#state.count + 1, 64 | {reply, ok, State#state{count=C}}; 65 | handle_call({count}, _From, State) -> 66 | {reply, State#state.count, State}; 67 | handle_call(_Request, _From, State) -> 68 | {reply, ok, State}. 69 | 70 | %% @private 71 | handle_cast({crash}, State) -> 72 | {stop, crash, State}; 73 | handle_cast({stop}, State) -> 74 | {stop, normal, State}. 75 | 76 | %% @private 77 | handle_info(_Info, State) -> 78 | {noreply, State}. 79 | 80 | %% @private 81 | terminate(_Reason, _State) -> 82 | ok. 83 | 84 | %% @private 85 | code_change(_OldVsn, State, _Extra) -> 86 | {ok, State}. 87 | --------------------------------------------------------------------------------