├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── UNLICENSE ├── VERSION ├── rebar.config ├── rebar.lock ├── rebar3 ├── src ├── poolboy.app.src ├── poolboy.erl ├── poolboy_sup.erl └── poolboy_worker.erl └── test ├── poolboy_eqc.erl ├── poolboy_test_worker.erl └── poolboy_tests.erl /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | # 4 space indentation 12 | [*.{erl,src}] 13 | indent_style = space 14 | indent_size = 4 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .eunit 2 | .rebar 3 | _build 4 | ebin 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | otp_release: 3 | - 21.0.4 4 | - 20.3.8.2 5 | - 19.3 6 | - 18.3 7 | - 17.4 8 | sudo: false 9 | script: 10 | - ./rebar3 do compile, dialyzer 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2014, Devin Alexander Torres 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REBAR = $(shell command -v rebar3 || echo ./rebar3) 2 | 3 | .PHONY: all compile test qc clean 4 | 5 | all: compile 6 | 7 | compile: 8 | @$(REBAR) compile 9 | 10 | test: 11 | @$(REBAR) eunit 12 | 13 | qc: compile 14 | @$(REBAR) eqc 15 | 16 | clean: 17 | @$(REBAR) clean 18 | 19 | dialyze: 20 | @$(REBAR) dialyzer 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Poolboy - A hunky Erlang worker pool factory 2 | 3 | [![Build Status](https://api.travis-ci.org/devinus/poolboy.svg?branch=master)](https://travis-ci.org/devinus/poolboy) 4 | 5 | [![Support via Gratipay](https://cdn.rawgit.com/gratipay/gratipay-badge/2.3.0/dist/gratipay.png)](https://gratipay.com/devinus/) 6 | 7 | Poolboy is a **lightweight**, **generic** pooling library for Erlang with a 8 | focus on **simplicity**, **performance**, and **rock-solid** disaster recovery. 9 | 10 | ## Usage 11 | 12 | ```erl-sh 13 | 1> Worker = poolboy:checkout(PoolName). 14 | <0.9001.0> 15 | 2> gen_server:call(Worker, Request). 16 | ok 17 | 3> poolboy:checkin(PoolName, Worker). 18 | ok 19 | ``` 20 | 21 | ## Example 22 | 23 | This is an example application showcasing database connection pools using 24 | Poolboy and [epgsql](https://github.com/epgsql/epgsql). 25 | 26 | ### example.app 27 | 28 | ```erlang 29 | {application, example, [ 30 | {description, "An example application"}, 31 | {vsn, "0.1"}, 32 | {applications, [kernel, stdlib, sasl, crypto, ssl]}, 33 | {modules, [example, example_worker]}, 34 | {registered, [example]}, 35 | {mod, {example, []}}, 36 | {env, [ 37 | {pools, [ 38 | {pool1, [ 39 | {size, 10}, 40 | {max_overflow, 20} 41 | ], [ 42 | {hostname, "127.0.0.1"}, 43 | {database, "db1"}, 44 | {username, "db1"}, 45 | {password, "abc123"} 46 | ]}, 47 | {pool2, [ 48 | {size, 5}, 49 | {max_overflow, 10} 50 | ], [ 51 | {hostname, "127.0.0.1"}, 52 | {database, "db2"}, 53 | {username, "db2"}, 54 | {password, "abc123"} 55 | ]} 56 | ]} 57 | ]} 58 | ]}. 59 | ``` 60 | 61 | ### example.erl 62 | 63 | ```erlang 64 | -module(example). 65 | -behaviour(application). 66 | -behaviour(supervisor). 67 | 68 | -export([start/0, stop/0, squery/2, equery/3]). 69 | -export([start/2, stop/1]). 70 | -export([init/1]). 71 | 72 | start() -> 73 | application:start(?MODULE). 74 | 75 | stop() -> 76 | application:stop(?MODULE). 77 | 78 | start(_Type, _Args) -> 79 | supervisor:start_link({local, example_sup}, ?MODULE, []). 80 | 81 | stop(_State) -> 82 | ok. 83 | 84 | init([]) -> 85 | {ok, Pools} = application:get_env(example, pools), 86 | PoolSpecs = lists:map(fun({Name, SizeArgs, WorkerArgs}) -> 87 | PoolArgs = [{name, {local, Name}}, 88 | {worker_module, example_worker}] ++ SizeArgs, 89 | poolboy:child_spec(Name, PoolArgs, WorkerArgs) 90 | end, Pools), 91 | {ok, {{one_for_one, 10, 10}, PoolSpecs}}. 92 | 93 | squery(PoolName, Sql) -> 94 | poolboy:transaction(PoolName, fun(Worker) -> 95 | gen_server:call(Worker, {squery, Sql}) 96 | end). 97 | 98 | equery(PoolName, Stmt, Params) -> 99 | poolboy:transaction(PoolName, fun(Worker) -> 100 | gen_server:call(Worker, {equery, Stmt, Params}) 101 | end). 102 | ``` 103 | 104 | ### example_worker.erl 105 | 106 | ```erlang 107 | -module(example_worker). 108 | -behaviour(gen_server). 109 | -behaviour(poolboy_worker). 110 | 111 | -export([start_link/1]). 112 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, 113 | code_change/3]). 114 | 115 | -record(state, {conn}). 116 | 117 | start_link(Args) -> 118 | gen_server:start_link(?MODULE, Args, []). 119 | 120 | init(Args) -> 121 | process_flag(trap_exit, true), 122 | Hostname = proplists:get_value(hostname, Args), 123 | Database = proplists:get_value(database, Args), 124 | Username = proplists:get_value(username, Args), 125 | Password = proplists:get_value(password, Args), 126 | {ok, Conn} = epgsql:connect(Hostname, Username, Password, [ 127 | {database, Database} 128 | ]), 129 | {ok, #state{conn=Conn}}. 130 | 131 | handle_call({squery, Sql}, _From, #state{conn=Conn}=State) -> 132 | {reply, epgsql:squery(Conn, Sql), State}; 133 | handle_call({equery, Stmt, Params}, _From, #state{conn=Conn}=State) -> 134 | {reply, epgsql:equery(Conn, Stmt, Params), State}; 135 | handle_call(_Request, _From, State) -> 136 | {reply, ok, State}. 137 | 138 | handle_cast(_Msg, State) -> 139 | {noreply, State}. 140 | 141 | handle_info(_Info, State) -> 142 | {noreply, State}. 143 | 144 | terminate(_Reason, #state{conn=Conn}) -> 145 | ok = epgsql:close(Conn), 146 | ok. 147 | 148 | code_change(_OldVsn, State, _Extra) -> 149 | {ok, State}. 150 | ``` 151 | 152 | ## Options 153 | 154 | - `name`: the pool name 155 | - `worker_module`: the module that represents the workers 156 | - `size`: maximum pool size 157 | - `max_overflow`: maximum number of workers created if pool is empty 158 | - `strategy`: `lifo` or `fifo`, determines whether checked in workers should be 159 | placed first or last in the line of available workers. So, `lifo` operates like a traditional stack; `fifo` like a queue. Default is `lifo`. 160 | 161 | ## Authors 162 | 163 | - Devin Torres (devinus) 164 | - Andrew Thompson (Vagabond) 165 | - Kurt Williams (onkel-dirtus) 166 | 167 | ## License 168 | 169 | Poolboy is available in the public domain (see `UNLICENSE`). 170 | Poolboy is also optionally available under the ISC license (see `LICENSE`), 171 | meant especially for jurisdictions that do not recognize public domain works. 172 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.5.1 2 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [ 2 | debug_info, 3 | {platform_define, "^R", pre17} 4 | ]}. 5 | 6 | {eunit_opts, [verbose]}. 7 | {cover_enabled, true}. 8 | 9 | {profiles, [ 10 | {test, [ 11 | {plugins, [ 12 | {rebar3_eqc, ".*", {git, "https://github.com/kellymclaughlin/rebar3-eqc-plugin.git", {tag, "0.1.0"}}} 13 | ]} 14 | ] 15 | }]}. 16 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | []. 2 | -------------------------------------------------------------------------------- /rebar3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devinus/poolboy/9212a8770edb149ee7ca0bca353855e215f7cba5/rebar3 -------------------------------------------------------------------------------- /src/poolboy.app.src: -------------------------------------------------------------------------------- 1 | {application, poolboy, [ 2 | {description, "A hunky Erlang worker pool factory"}, 3 | {vsn, "1.5.2"}, 4 | {applications, [kernel, stdlib]}, 5 | {registered, [poolboy]}, 6 | 7 | {maintainers, ["Devin Torres", "Andrew Thompson", "Kurt Williams"]}, 8 | {licenses, ["Unlicense", "Apache 2.0"]}, 9 | {links, [{"GitHub", "https://github.com/devinus/poolboy"}]} 10 | ]}. 11 | -------------------------------------------------------------------------------- /src/poolboy.erl: -------------------------------------------------------------------------------- 1 | %% Poolboy - A hunky Erlang worker pool factory 2 | 3 | -module(poolboy). 4 | -behaviour(gen_server). 5 | 6 | -export([checkout/1, checkout/2, checkout/3, checkin/2, transaction/2, 7 | transaction/3, child_spec/2, child_spec/3, child_spec/4, start/1, 8 | start/2, start_link/1, start_link/2, stop/1, status/1]). 9 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, 10 | code_change/3]). 11 | -export_type([pool/0]). 12 | 13 | -define(TIMEOUT, 5000). 14 | 15 | -ifdef(pre17). 16 | -type pid_queue() :: queue(). 17 | -else. 18 | -type pid_queue() :: queue:queue(). 19 | -endif. 20 | 21 | -ifdef(OTP_RELEASE). %% this implies 21 or higher 22 | -define(EXCEPTION(Class, Reason, Stacktrace), Class:Reason:Stacktrace). 23 | -define(GET_STACK(Stacktrace), Stacktrace). 24 | -else. 25 | -define(EXCEPTION(Class, Reason, _), Class:Reason). 26 | -define(GET_STACK(_), erlang:get_stacktrace()). 27 | -endif. 28 | 29 | -type pool() :: 30 | Name :: (atom() | pid()) | 31 | {Name :: atom(), node()} | 32 | {local, Name :: atom()} | 33 | {global, GlobalName :: any()} | 34 | {via, Module :: atom(), ViaName :: any()}. 35 | 36 | % Copied from gen:start_ret/0 37 | -type start_ret() :: {'ok', pid()} | 'ignore' | {'error', term()}. 38 | 39 | -record(state, { 40 | supervisor :: undefined | pid(), 41 | workers :: undefined | pid_queue(), 42 | waiting :: pid_queue(), 43 | monitors :: ets:tid(), 44 | size = 5 :: non_neg_integer(), 45 | overflow = 0 :: non_neg_integer(), 46 | max_overflow = 10 :: non_neg_integer(), 47 | strategy = lifo :: lifo | fifo 48 | }). 49 | 50 | -spec checkout(Pool :: pool()) -> pid(). 51 | checkout(Pool) -> 52 | checkout(Pool, true). 53 | 54 | -spec checkout(Pool :: pool(), Block :: boolean()) -> pid() | full. 55 | checkout(Pool, Block) -> 56 | checkout(Pool, Block, ?TIMEOUT). 57 | 58 | -spec checkout(Pool :: pool(), Block :: boolean(), Timeout :: timeout()) 59 | -> pid() | full. 60 | checkout(Pool, Block, Timeout) -> 61 | CRef = make_ref(), 62 | try 63 | gen_server:call(Pool, {checkout, CRef, Block}, Timeout) 64 | catch 65 | ?EXCEPTION(Class, Reason, Stacktrace) -> 66 | gen_server:cast(Pool, {cancel_waiting, CRef}), 67 | erlang:raise(Class, Reason, ?GET_STACK(Stacktrace)) 68 | end. 69 | 70 | -spec checkin(Pool :: pool(), Worker :: pid()) -> ok. 71 | checkin(Pool, Worker) when is_pid(Worker) -> 72 | gen_server:cast(Pool, {checkin, Worker}). 73 | 74 | -spec transaction(Pool :: pool(), Fun :: fun((Worker :: pid()) -> any())) 75 | -> any(). 76 | transaction(Pool, Fun) -> 77 | transaction(Pool, Fun, ?TIMEOUT). 78 | 79 | -spec transaction(Pool :: pool(), Fun :: fun((Worker :: pid()) -> any()), 80 | Timeout :: timeout()) -> any(). 81 | transaction(Pool, Fun, Timeout) -> 82 | Worker = poolboy:checkout(Pool, true, Timeout), 83 | try 84 | Fun(Worker) 85 | after 86 | ok = poolboy:checkin(Pool, Worker) 87 | end. 88 | 89 | -spec child_spec(PoolId :: term(), PoolArgs :: proplists:proplist()) 90 | -> supervisor:child_spec(). 91 | child_spec(PoolId, PoolArgs) -> 92 | child_spec(PoolId, PoolArgs, []). 93 | 94 | -spec child_spec(PoolId :: term(), 95 | PoolArgs :: proplists:proplist(), 96 | WorkerArgs :: proplists:proplist()) 97 | -> supervisor:child_spec(). 98 | child_spec(PoolId, PoolArgs, WorkerArgs) -> 99 | child_spec(PoolId, PoolArgs, WorkerArgs, tuple). 100 | 101 | -spec child_spec(PoolId :: term(), 102 | PoolArgs :: proplists:proplist(), 103 | WorkerArgs :: proplists:proplist(), 104 | ChildSpecFormat :: 'tuple' | 'map') 105 | -> supervisor:child_spec(). 106 | child_spec(PoolId, PoolArgs, WorkerArgs, tuple) -> 107 | {PoolId, {poolboy, start_link, [PoolArgs, WorkerArgs]}, 108 | permanent, 5000, worker, [poolboy]}; 109 | child_spec(PoolId, PoolArgs, WorkerArgs, map) -> 110 | #{id => PoolId, 111 | start => {poolboy, start_link, [PoolArgs, WorkerArgs]}, 112 | restart => permanent, 113 | shutdown => 5000, 114 | type => worker, 115 | modules => [poolboy]}. 116 | 117 | -spec start(PoolArgs :: proplists:proplist()) 118 | -> start_ret(). 119 | start(PoolArgs) -> 120 | start(PoolArgs, PoolArgs). 121 | 122 | -spec start(PoolArgs :: proplists:proplist(), 123 | WorkerArgs:: proplists:proplist()) 124 | -> start_ret(). 125 | start(PoolArgs, WorkerArgs) -> 126 | start_pool(start, PoolArgs, WorkerArgs). 127 | 128 | -spec start_link(PoolArgs :: proplists:proplist()) 129 | -> start_ret(). 130 | start_link(PoolArgs) -> 131 | %% for backwards compatability, pass the pool args as the worker args as well 132 | start_link(PoolArgs, PoolArgs). 133 | 134 | -spec start_link(PoolArgs :: proplists:proplist(), 135 | WorkerArgs:: proplists:proplist()) 136 | -> start_ret(). 137 | start_link(PoolArgs, WorkerArgs) -> 138 | start_pool(start_link, PoolArgs, WorkerArgs). 139 | 140 | -spec stop(Pool :: pool()) -> ok. 141 | stop(Pool) -> 142 | gen_server:call(Pool, stop). 143 | 144 | -spec status(Pool :: pool()) -> {atom(), integer(), integer(), integer()}. 145 | status(Pool) -> 146 | gen_server:call(Pool, status). 147 | 148 | init({PoolArgs, WorkerArgs}) -> 149 | process_flag(trap_exit, true), 150 | Waiting = queue:new(), 151 | Monitors = ets:new(monitors, [private]), 152 | init(PoolArgs, WorkerArgs, #state{waiting = Waiting, monitors = Monitors}). 153 | 154 | init([{worker_module, Mod} | Rest], WorkerArgs, State) when is_atom(Mod) -> 155 | {ok, Sup} = poolboy_sup:start_link(Mod, WorkerArgs), 156 | init(Rest, WorkerArgs, State#state{supervisor = Sup}); 157 | init([{size, Size} | Rest], WorkerArgs, State) when is_integer(Size) -> 158 | init(Rest, WorkerArgs, State#state{size = Size}); 159 | init([{max_overflow, MaxOverflow} | Rest], WorkerArgs, State) when is_integer(MaxOverflow) -> 160 | init(Rest, WorkerArgs, State#state{max_overflow = MaxOverflow}); 161 | init([{strategy, lifo} | Rest], WorkerArgs, State) -> 162 | init(Rest, WorkerArgs, State#state{strategy = lifo}); 163 | init([{strategy, fifo} | Rest], WorkerArgs, State) -> 164 | init(Rest, WorkerArgs, State#state{strategy = fifo}); 165 | init([_ | Rest], WorkerArgs, State) -> 166 | init(Rest, WorkerArgs, State); 167 | init([], _WorkerArgs, #state{size = Size, supervisor = Sup} = State) -> 168 | Workers = prepopulate(Size, Sup), 169 | {ok, State#state{workers = Workers}}. 170 | 171 | handle_cast({checkin, Pid}, State = #state{monitors = Monitors}) -> 172 | case ets:lookup(Monitors, Pid) of 173 | [{Pid, _, MRef}] -> 174 | true = erlang:demonitor(MRef), 175 | true = ets:delete(Monitors, Pid), 176 | NewState = handle_checkin(Pid, State), 177 | {noreply, NewState}; 178 | [] -> 179 | {noreply, State} 180 | end; 181 | 182 | handle_cast({cancel_waiting, CRef}, State) -> 183 | case ets:match(State#state.monitors, {'$1', CRef, '$2'}) of 184 | [[Pid, MRef]] -> 185 | demonitor(MRef, [flush]), 186 | true = ets:delete(State#state.monitors, Pid), 187 | NewState = handle_checkin(Pid, State), 188 | {noreply, NewState}; 189 | [] -> 190 | Cancel = fun({_, Ref, MRef}) when Ref =:= CRef -> 191 | demonitor(MRef, [flush]), 192 | false; 193 | (_) -> 194 | true 195 | end, 196 | Waiting = queue:filter(Cancel, State#state.waiting), 197 | {noreply, State#state{waiting = Waiting}} 198 | end; 199 | 200 | handle_cast(_Msg, State) -> 201 | {noreply, State}. 202 | 203 | handle_call({checkout, CRef, Block}, {FromPid, _} = From, State) -> 204 | #state{supervisor = Sup, 205 | workers = Workers, 206 | monitors = Monitors, 207 | overflow = Overflow, 208 | max_overflow = MaxOverflow, 209 | strategy = Strategy} = State, 210 | case get_worker_with_strategy(Workers, Strategy) of 211 | {{value, Pid}, Left} -> 212 | MRef = erlang:monitor(process, FromPid), 213 | true = ets:insert(Monitors, {Pid, CRef, MRef}), 214 | {reply, Pid, State#state{workers = Left}}; 215 | {empty, _Left} when MaxOverflow > 0, Overflow < MaxOverflow -> 216 | {Pid, MRef} = new_worker(Sup, FromPid), 217 | true = ets:insert(Monitors, {Pid, CRef, MRef}), 218 | {reply, Pid, State#state{overflow = Overflow + 1}}; 219 | {empty, _Left} when Block =:= false -> 220 | {reply, full, State}; 221 | {empty, _Left} -> 222 | MRef = erlang:monitor(process, FromPid), 223 | Waiting = queue:in({From, CRef, MRef}, State#state.waiting), 224 | {noreply, State#state{waiting = Waiting}} 225 | end; 226 | 227 | handle_call(status, _From, State) -> 228 | #state{workers = Workers, 229 | monitors = Monitors, 230 | overflow = Overflow} = State, 231 | StateName = state_name(State), 232 | {reply, {StateName, queue:len(Workers), Overflow, ets:info(Monitors, size)}, State}; 233 | handle_call(get_avail_workers, _From, State) -> 234 | Workers = State#state.workers, 235 | {reply, Workers, State}; 236 | handle_call(get_all_workers, _From, State) -> 237 | Sup = State#state.supervisor, 238 | WorkerList = supervisor:which_children(Sup), 239 | {reply, WorkerList, State}; 240 | handle_call(get_all_monitors, _From, State) -> 241 | Monitors = ets:select(State#state.monitors, 242 | [{{'$1', '_', '$2'}, [], [{{'$1', '$2'}}]}]), 243 | {reply, Monitors, State}; 244 | handle_call(stop, _From, State) -> 245 | {stop, normal, ok, State}; 246 | handle_call(_Msg, _From, State) -> 247 | Reply = {error, invalid_message}, 248 | {reply, Reply, State}. 249 | 250 | handle_info({'DOWN', MRef, _, _, _}, State) -> 251 | case ets:match(State#state.monitors, {'$1', '_', MRef}) of 252 | [[Pid]] -> 253 | true = ets:delete(State#state.monitors, Pid), 254 | NewState = handle_checkin(Pid, State), 255 | {noreply, NewState}; 256 | [] -> 257 | Waiting = queue:filter(fun ({_, _, R}) -> R =/= MRef end, State#state.waiting), 258 | {noreply, State#state{waiting = Waiting}} 259 | end; 260 | handle_info({'EXIT', Pid, _Reason}, State) -> 261 | #state{supervisor = Sup, 262 | monitors = Monitors} = State, 263 | case ets:lookup(Monitors, Pid) of 264 | [{Pid, _, MRef}] -> 265 | true = erlang:demonitor(MRef), 266 | true = ets:delete(Monitors, Pid), 267 | NewState = handle_worker_exit(Pid, State), 268 | {noreply, NewState}; 269 | [] -> 270 | case queue:member(Pid, State#state.workers) of 271 | true -> 272 | W = filter_worker_by_pid(Pid, State#state.workers), 273 | {noreply, State#state{workers = queue:in(new_worker(Sup), W)}}; 274 | false -> 275 | {noreply, State} 276 | end 277 | end; 278 | 279 | handle_info(_Info, State) -> 280 | {noreply, State}. 281 | 282 | terminate(_Reason, State) -> 283 | Workers = queue:to_list(State#state.workers), 284 | ok = lists:foreach(fun (W) -> unlink(W) end, Workers), 285 | true = exit(State#state.supervisor, shutdown), 286 | ok. 287 | 288 | code_change(_OldVsn, State, _Extra) -> 289 | {ok, State}. 290 | 291 | start_pool(StartFun, PoolArgs, WorkerArgs) -> 292 | case proplists:get_value(name, PoolArgs) of 293 | undefined -> 294 | gen_server:StartFun(?MODULE, {PoolArgs, WorkerArgs}, []); 295 | Name -> 296 | gen_server:StartFun(Name, ?MODULE, {PoolArgs, WorkerArgs}, []) 297 | end. 298 | 299 | new_worker(Sup) -> 300 | {ok, Pid} = supervisor:start_child(Sup, []), 301 | true = link(Pid), 302 | Pid. 303 | 304 | new_worker(Sup, FromPid) -> 305 | Pid = new_worker(Sup), 306 | Ref = erlang:monitor(process, FromPid), 307 | {Pid, Ref}. 308 | 309 | get_worker_with_strategy(Workers, fifo) -> 310 | queue:out(Workers); 311 | get_worker_with_strategy(Workers, lifo) -> 312 | queue:out_r(Workers). 313 | 314 | dismiss_worker(Sup, Pid) -> 315 | true = unlink(Pid), 316 | supervisor:terminate_child(Sup, Pid). 317 | 318 | filter_worker_by_pid(Pid, Workers) -> 319 | queue:filter(fun (WPid) -> WPid =/= Pid end, Workers). 320 | 321 | prepopulate(N, _Sup) when N < 1 -> 322 | queue:new(); 323 | prepopulate(N, Sup) -> 324 | prepopulate(N, Sup, queue:new()). 325 | 326 | prepopulate(0, _Sup, Workers) -> 327 | Workers; 328 | prepopulate(N, Sup, Workers) -> 329 | prepopulate(N-1, Sup, queue:in(new_worker(Sup), Workers)). 330 | 331 | handle_checkin(Pid, State) -> 332 | #state{supervisor = Sup, 333 | waiting = Waiting, 334 | monitors = Monitors, 335 | overflow = Overflow} = State, 336 | case queue:out(Waiting) of 337 | {{value, {From, CRef, MRef}}, Left} -> 338 | true = ets:insert(Monitors, {Pid, CRef, MRef}), 339 | gen_server:reply(From, Pid), 340 | State#state{waiting = Left}; 341 | {empty, Empty} when Overflow > 0 -> 342 | ok = dismiss_worker(Sup, Pid), 343 | State#state{waiting = Empty, overflow = Overflow - 1}; 344 | {empty, Empty} -> 345 | Workers = queue:in(Pid, State#state.workers), 346 | State#state{workers = Workers, waiting = Empty, overflow = 0} 347 | end. 348 | 349 | handle_worker_exit(Pid, State) -> 350 | #state{supervisor = Sup, 351 | monitors = Monitors, 352 | overflow = Overflow} = State, 353 | case queue:out(State#state.waiting) of 354 | {{value, {From, CRef, MRef}}, LeftWaiting} -> 355 | NewWorker = new_worker(State#state.supervisor), 356 | true = ets:insert(Monitors, {NewWorker, CRef, MRef}), 357 | gen_server:reply(From, NewWorker), 358 | State#state{waiting = LeftWaiting}; 359 | {empty, Empty} when Overflow > 0 -> 360 | State#state{overflow = Overflow - 1, waiting = Empty}; 361 | {empty, Empty} -> 362 | W = filter_worker_by_pid(Pid, State#state.workers), 363 | Workers = queue:in(new_worker(Sup), W), 364 | State#state{workers = Workers, waiting = Empty} 365 | end. 366 | 367 | state_name(State = #state{overflow = Overflow}) when Overflow < 1 -> 368 | #state{max_overflow = MaxOverflow, workers = Workers} = State, 369 | case queue:len(Workers) == 0 of 370 | true when MaxOverflow < 1 -> full; 371 | true -> overflow; 372 | false -> ready 373 | end; 374 | state_name(#state{overflow = MaxOverflow, max_overflow = MaxOverflow}) -> 375 | full; 376 | state_name(_State) -> 377 | overflow. 378 | -------------------------------------------------------------------------------- /src/poolboy_sup.erl: -------------------------------------------------------------------------------- 1 | %% Poolboy - A hunky Erlang worker pool factory 2 | 3 | -module(poolboy_sup). 4 | -behaviour(supervisor). 5 | 6 | -export([start_link/2, init/1]). 7 | 8 | start_link(Mod, Args) -> 9 | supervisor:start_link(?MODULE, {Mod, Args}). 10 | 11 | init({Mod, Args}) -> 12 | {ok, {{simple_one_for_one, 0, 1}, 13 | [{Mod, {Mod, start_link, [Args]}, 14 | temporary, 5000, worker, [Mod]}]}}. 15 | -------------------------------------------------------------------------------- /src/poolboy_worker.erl: -------------------------------------------------------------------------------- 1 | %% Poolboy - A hunky Erlang worker pool factory 2 | 3 | -module(poolboy_worker). 4 | 5 | -callback start_link(WorkerArgs) -> {ok, Pid} | 6 | {error, {already_started, Pid}} | 7 | {error, Reason} when 8 | WorkerArgs :: proplists:proplist(), 9 | Pid :: pid(), 10 | Reason :: term(). 11 | -------------------------------------------------------------------------------- /test/poolboy_eqc.erl: -------------------------------------------------------------------------------- 1 | -module(poolboy_eqc). 2 | -compile([export_all, nowarn_export_all]). 3 | 4 | -ifdef(TEST). 5 | -ifdef(EQC). 6 | -include_lib("eqc/include/eqc.hrl"). 7 | -include_lib("eqc/include/eqc_statem.hrl"). 8 | 9 | -include_lib("eunit/include/eunit.hrl"). 10 | 11 | poolboy_test_() -> 12 | {timeout, 20, 13 | fun() -> 14 | ?assert(eqc:quickcheck(eqc:testing_time(4, 15 | poolboy_eqc:prop_sequential()))), 16 | ?assert(eqc:quickcheck(eqc:testing_time(4, 17 | poolboy_eqc:prop_parallel()))) 18 | end 19 | }. 20 | 21 | -record(state, 22 | { 23 | pid, 24 | size, 25 | max_overflow, 26 | checked_out = [] 27 | }). 28 | 29 | initial_state() -> 30 | #state{}. 31 | 32 | command(S) -> 33 | oneof( 34 | [{call, ?MODULE, start_poolboy, make_args(S, nat(), nat())} || S#state.pid == undefined] ++ 35 | [{call, ?MODULE, stop_poolboy, [S#state.pid]} || S#state.pid /= undefined] ++ 36 | [{call, ?MODULE, checkout_nonblock, [S#state.pid]} || S#state.pid /= undefined] ++ 37 | %% checkout shrinks to checkout_nonblock so we can simplify counterexamples 38 | [{call, ?MODULE, ?SHRINK(checkout_block, [checkout_nonblock]), [S#state.pid]} || S#state.pid /= undefined] ++ 39 | [{call, ?MODULE, checkin, [S#state.pid, fault({call, ?MODULE, spawn_process, []}, elements(S#state.checked_out))]} || S#state.pid /= undefined, S#state.checked_out /= []] ++ 40 | [{call, ?MODULE, kill_worker, [elements(S#state.checked_out)]} || S#state.pid /= undefined, S#state.checked_out /= []] ++ 41 | [{call, ?MODULE, kill_idle_worker, [S#state.pid]} || S#state.pid /= undefined] ++ 42 | [{call, ?MODULE, spurious_exit, [S#state.pid]} || S#state.pid /= undefined] 43 | ). 44 | 45 | make_args(_S, Size, Overflow) -> 46 | [[{size, Size}, {max_overflow, Overflow}, {worker_module, poolboy_test_worker}, {name, {local, poolboy_eqc}}]]. 47 | 48 | spawn_process() -> 49 | {spawn(fun() -> 50 | timer:sleep(5000) 51 | end), self()}. 52 | 53 | spawn_linked_process(Pool) -> 54 | Parent = self(), 55 | Pid = spawn(fun() -> 56 | link(Pool), 57 | Parent ! {linked, self()}, 58 | timer:sleep(5000) 59 | end), 60 | receive 61 | {linked, Pid} -> 62 | Pid 63 | end. 64 | 65 | start_poolboy(Args) -> 66 | {ok, Pid} = poolboy:start_link(Args), 67 | Pid. 68 | 69 | stop_poolboy(Pid) -> 70 | gen_server:call(Pid, stop), 71 | timer:sleep(1). 72 | 73 | checkout_nonblock(Pool) -> 74 | {poolboy:checkout(Pool, false), self()}. 75 | 76 | checkout_block(Pool) -> 77 | {catch(poolboy:checkout(Pool, true, 100)), self()}. 78 | 79 | checkin(Pool, {Worker, _}) -> 80 | Res = poolboy:checkin(Pool, Worker), 81 | gen_server:call(Pool, get_avail_workers), 82 | Res. 83 | 84 | kill_worker({Worker, _}) -> 85 | exit(Worker, kill), 86 | timer:sleep(1), 87 | Worker. 88 | 89 | kill_idle_worker(Pool) -> 90 | Pid = poolboy:checkout(Pool, false), 91 | case Pid of 92 | _ when is_pid(Pid) -> 93 | poolboy:checkin(Pool, Pid), 94 | kill_worker({Pid, self()}); 95 | _ -> 96 | timer:sleep(1), 97 | kill_idle_worker(Pool) 98 | end. 99 | 100 | spurious_exit(Pool) -> 101 | Pid = spawn_linked_process(Pool), 102 | exit(Pid, kill). 103 | 104 | precondition(S,{call,_,start_poolboy,_}) -> 105 | %% only start new pool when old one is stopped 106 | S#state.pid == undefined; 107 | precondition(S,_) when S#state.pid == undefined -> 108 | %% all other states need a running pool 109 | false; 110 | precondition(S, {call, _, kill_worker, [Pid]}) -> 111 | lists:member(Pid, S#state.checked_out); 112 | precondition(S,{call,_,kill_idle_worker,[_Pool]}) -> 113 | length(S#state.checked_out) < S#state.size; 114 | precondition(S,{call,_,checkin,[_Pool, Pid]}) -> 115 | lists:member(Pid, S#state.checked_out); 116 | precondition(_S,{call,_,_,_}) -> 117 | true. 118 | 119 | %% check model state against internal state, only used in sequential tests 120 | invariant(S = #state{pid=Pid},_) when Pid /= undefined -> 121 | State = if length(S#state.checked_out) == S#state.size + S#state.max_overflow -> 122 | full; 123 | length(S#state.checked_out) >= S#state.size -> 124 | overflow; 125 | true -> 126 | ready 127 | end, 128 | 129 | Workers = max(0, S#state.size - length(S#state.checked_out)), 130 | OverFlow = max(0, length(S#state.checked_out) - S#state.size), 131 | Monitors = length(S#state.checked_out), 132 | 133 | RealStatus = gen_server:call(Pid, status), 134 | case RealStatus == {State, Workers, OverFlow, Monitors} of 135 | true -> 136 | true; 137 | _ -> 138 | {wrong_state, RealStatus, {State, Workers, OverFlow, Monitors}} 139 | end; 140 | invariant(_,_) -> 141 | true. 142 | 143 | %% what states block 144 | blocking(S, {call, _, checkout_block, _}) -> 145 | %% blocking checkout can block if we expect a checkout to fail 146 | not checkout_ok(S); 147 | blocking(_, _) -> 148 | false. 149 | 150 | postcondition(S,{call,_,checkout_block,[_Pool]},R) -> 151 | case R of 152 | {{'EXIT', {timeout, _}}, _} -> 153 | case length(S#state.checked_out) >= S#state.size + S#state.max_overflow of 154 | true -> 155 | true; 156 | _ -> 157 | {checkout_block, R} 158 | end; 159 | _ -> 160 | case length(S#state.checked_out) < S#state.size + S#state.max_overflow of 161 | true -> 162 | true; 163 | _ -> 164 | {checkout_block, R} 165 | end 166 | end; 167 | postcondition(S,{call,_,checkout_nonblock,[_Pool]},R) -> 168 | case R of 169 | {full, _} -> 170 | case length(S#state.checked_out) >= S#state.size + S#state.max_overflow of 171 | true -> 172 | true; 173 | _ -> 174 | {checkout_nonblock, R} 175 | end; 176 | _ -> 177 | case length(S#state.checked_out) < S#state.size + S#state.max_overflow of 178 | true -> 179 | true; 180 | _ -> 181 | {checkout_block, R} 182 | end 183 | end; 184 | postcondition(_S, {call,_,checkin,_}, R) -> 185 | case R of 186 | ok -> 187 | true; 188 | _ -> 189 | {checkin, R} 190 | end; 191 | postcondition(_S,{call,_,_,_},_R) -> 192 | true. 193 | 194 | next_state(S,V,{call,_,start_poolboy, [Args]}) -> 195 | S#state{pid=V, 196 | size=proplists:get_value(size, Args), 197 | max_overflow=proplists:get_value(max_overflow, Args) 198 | }; 199 | next_state(S,_V,{call,_,stop_poolboy, [_Args]}) -> 200 | S#state{pid=undefined, checked_out=[]}; 201 | next_state(S,V,{call,_,checkout_block,_}) -> 202 | %% if the model says the checkout worked, store the result 203 | case checkout_ok(S) of 204 | false -> 205 | S; 206 | _ -> 207 | S#state{checked_out=S#state.checked_out++[V]} 208 | end; 209 | next_state(S,V,{call,_,checkout_nonblock,_}) -> 210 | %% if the model says the checkout worked, store the result 211 | case checkout_ok(S) of 212 | false -> 213 | S; 214 | _ -> 215 | S#state{checked_out=S#state.checked_out++[V]} 216 | end; 217 | next_state(S,_V,{call, _, checkin, [_Pool, Worker]}) -> 218 | S#state{checked_out=S#state.checked_out -- [Worker]}; 219 | next_state(S,_V,{call, _, kill_worker, [Worker]}) -> 220 | S#state{checked_out=S#state.checked_out -- [Worker]}; 221 | next_state(S,_V,{call, _, kill_idle_worker, [_Pool]}) -> 222 | S; 223 | next_state(S,_V,{call, _, spurious_exit, [_Pool]}) -> 224 | S; 225 | next_state(S,V,{call, erlang, self, []}) -> 226 | %% added after test generation, values are never symbolic 227 | S#state{checked_out=[{Worker, Pid} || {Worker, Pid} <- S#state.checked_out, Pid /= V]}. 228 | 229 | 230 | prop_sequential() -> 231 | fault_rate(1, 10, 232 | ?FORALL(Cmds,commands(?MODULE), 233 | ?TRAPEXIT( 234 | aggregate(command_names(Cmds), 235 | begin 236 | {H,S,Res} = run_commands(?MODULE,Cmds), 237 | catch(stop_poolboy(whereis(poolboy_eqc))), 238 | ?WHENFAIL(io:format("History: ~p\nState: ~p\nRes: ~p\n~p\n", 239 | [H,S,Res, zip(Cmds, [Y || {_, Y} <- H])]), 240 | Res == ok) 241 | end)))). 242 | 243 | prop_parallel() -> 244 | fault_rate(1, 10, 245 | ?FORALL(Cmds={Seq,Par},parallel_commands(?MODULE), 246 | ?TRAPEXIT( 247 | aggregate(command_names(Cmds), 248 | begin 249 | NewPar = [P ++ [{set, {var, 0}, {call, erlang, self, []}}] || P <- Par], 250 | {H,S,Res} = run_parallel_commands(?MODULE,{Seq,NewPar}), 251 | catch(stop_poolboy(whereis(poolboy_eqc))), 252 | ?WHENFAIL(io:format("History: ~p\nState: ~p\nRes: ~p\n", 253 | [H,S,Res]), 254 | Res == ok) 255 | end)))). 256 | 257 | 258 | checkout_ok(S) -> 259 | length(S#state.checked_out) < S#state.size + S#state.max_overflow. 260 | 261 | -endif. 262 | -endif. 263 | -------------------------------------------------------------------------------- /test/poolboy_test_worker.erl: -------------------------------------------------------------------------------- 1 | -module(poolboy_test_worker). 2 | -behaviour(gen_server). 3 | -behaviour(poolboy_worker). 4 | 5 | -export([start_link/1]). 6 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, 7 | code_change/3]). 8 | 9 | start_link(_Args) -> 10 | gen_server:start_link(?MODULE, [], []). 11 | 12 | init([]) -> 13 | {ok, undefined}. 14 | 15 | handle_call(die, _From, State) -> 16 | {stop, {error, died}, dead, State}; 17 | handle_call(_Event, _From, State) -> 18 | {reply, ok, State}. 19 | 20 | handle_cast(_Event, State) -> 21 | {noreply, State}. 22 | 23 | handle_info(_Info, State) -> 24 | {noreply, State}. 25 | 26 | terminate(_Reason, _State) -> 27 | ok. 28 | 29 | code_change(_OldVsn, State, _Extra) -> 30 | {ok, State}. 31 | -------------------------------------------------------------------------------- /test/poolboy_tests.erl: -------------------------------------------------------------------------------- 1 | -module(poolboy_tests). 2 | 3 | -include_lib("eunit/include/eunit.hrl"). 4 | 5 | pool_test_() -> 6 | {foreach, 7 | fun() -> 8 | error_logger:tty(false) 9 | end, 10 | fun(_) -> 11 | case whereis(poolboy_test) of 12 | undefined -> ok; 13 | Pid -> pool_call(Pid, stop) 14 | end, 15 | error_logger:tty(true) 16 | end, 17 | [ 18 | {<<"Basic pool operations">>, 19 | fun pool_startup/0 20 | }, 21 | {<<"Pool overflow should work">>, 22 | fun pool_overflow/0 23 | }, 24 | {<<"Pool behaves when empty">>, 25 | fun pool_empty/0 26 | }, 27 | {<<"Pool behaves when empty and oveflow is disabled">>, 28 | fun pool_empty_no_overflow/0 29 | }, 30 | {<<"Pool behaves on worker death">>, 31 | fun worker_death/0 32 | }, 33 | {<<"Pool behaves when full and a worker dies">>, 34 | fun worker_death_while_full/0 35 | }, 36 | {<<"Pool behaves when full, a worker dies and overflow disabled">>, 37 | fun worker_death_while_full_no_overflow/0 38 | }, 39 | {<<"Non-blocking pool behaves when full and overflow disabled">>, 40 | fun pool_full_nonblocking_no_overflow/0 41 | }, 42 | {<<"Non-blocking pool behaves when full">>, 43 | fun pool_full_nonblocking/0 44 | }, 45 | {<<"Pool behaves on owner death">>, 46 | fun owner_death/0 47 | }, 48 | {<<"Worker checked-in after an exception in a transaction">>, 49 | fun checkin_after_exception_in_transaction/0 50 | }, 51 | {<<"Pool returns status">>, 52 | fun pool_returns_status/0 53 | }, 54 | {<<"Pool demonitors previously waiting processes">>, 55 | fun demonitors_previously_waiting_processes/0 56 | }, 57 | {<<"Pool demonitors when a checkout is cancelled">>, 58 | fun demonitors_when_checkout_cancelled/0 59 | }, 60 | {<<"Check that LIFO is the default strategy">>, 61 | fun default_strategy_lifo/0 62 | }, 63 | {<<"Check LIFO strategy">>, 64 | fun lifo_strategy/0 65 | }, 66 | {<<"Check FIFO strategy">>, 67 | fun fifo_strategy/0 68 | }, 69 | {<<"Pool reuses waiting monitor when a worker exits">>, 70 | fun reuses_waiting_monitor_on_worker_exit/0 71 | }, 72 | {<<"Recover from timeout without exit handling">>, 73 | fun transaction_timeout_without_exit/0}, 74 | {<<"Recover from transaction timeout">>, 75 | fun transaction_timeout/0} 76 | ] 77 | }. 78 | 79 | %% Tell a worker to exit and await its impending doom. 80 | kill_worker(Pid) -> 81 | erlang:monitor(process, Pid), 82 | pool_call(Pid, die), 83 | receive 84 | {'DOWN', _, process, Pid, _} -> 85 | ok 86 | end. 87 | 88 | checkin_worker(Pid, Worker) -> 89 | %% There's no easy way to wait for a checkin to complete, because it's 90 | %% async and the supervisor may kill the process if it was an overflow 91 | %% worker. The only solution seems to be a nasty hardcoded sleep. 92 | poolboy:checkin(Pid, Worker), 93 | timer:sleep(500). 94 | 95 | 96 | transaction_timeout_without_exit() -> 97 | {ok, Pid} = new_pool(1, 0), 98 | ?assertEqual({ready,1,0,0}, pool_call(Pid, status)), 99 | WorkerList = pool_call(Pid, get_all_workers), 100 | ?assertMatch([_], WorkerList), 101 | spawn(poolboy, transaction, [Pid, 102 | fun(Worker) -> 103 | ok = pool_call(Worker, work) 104 | end, 105 | 0]), 106 | timer:sleep(100), 107 | ?assertEqual(WorkerList, pool_call(Pid, get_all_workers)), 108 | ?assertEqual({ready,1,0,0}, pool_call(Pid, status)). 109 | 110 | 111 | transaction_timeout() -> 112 | {ok, Pid} = new_pool(1, 0), 113 | ?assertEqual({ready,1,0,0}, pool_call(Pid, status)), 114 | WorkerList = pool_call(Pid, get_all_workers), 115 | ?assertMatch([_], WorkerList), 116 | ?assertExit( 117 | {timeout, _}, 118 | poolboy:transaction(Pid, 119 | fun(Worker) -> 120 | ok = pool_call(Worker, work) 121 | end, 122 | 0)), 123 | ?assertEqual(WorkerList, pool_call(Pid, get_all_workers)), 124 | ?assertEqual({ready,1,0,0}, pool_call(Pid, status)). 125 | 126 | 127 | pool_startup() -> 128 | %% Check basic pool operation. 129 | {ok, Pid} = new_pool(10, 5), 130 | ?assertEqual(10, queue:len(pool_call(Pid, get_avail_workers))), 131 | poolboy:checkout(Pid), 132 | ?assertEqual(9, queue:len(pool_call(Pid, get_avail_workers))), 133 | Worker = poolboy:checkout(Pid), 134 | ?assertEqual(8, queue:len(pool_call(Pid, get_avail_workers))), 135 | checkin_worker(Pid, Worker), 136 | ?assertEqual(9, queue:len(pool_call(Pid, get_avail_workers))), 137 | ?assertEqual(1, length(pool_call(Pid, get_all_monitors))), 138 | ok = pool_call(Pid, stop). 139 | 140 | pool_overflow() -> 141 | %% Check that the pool overflows properly. 142 | {ok, Pid} = new_pool(5, 5), 143 | Workers = [poolboy:checkout(Pid) || _ <- lists:seq(0, 6)], 144 | ?assertEqual(0, queue:len(pool_call(Pid, get_avail_workers))), 145 | ?assertEqual(7, length(pool_call(Pid, get_all_workers))), 146 | [A, B, C, D, E, F, G] = Workers, 147 | checkin_worker(Pid, A), 148 | checkin_worker(Pid, B), 149 | ?assertEqual(0, queue:len(pool_call(Pid, get_avail_workers))), 150 | ?assertEqual(5, length(pool_call(Pid, get_all_workers))), 151 | checkin_worker(Pid, C), 152 | checkin_worker(Pid, D), 153 | ?assertEqual(2, queue:len(pool_call(Pid, get_avail_workers))), 154 | ?assertEqual(5, length(pool_call(Pid, get_all_workers))), 155 | checkin_worker(Pid, E), 156 | checkin_worker(Pid, F), 157 | ?assertEqual(4, queue:len(pool_call(Pid, get_avail_workers))), 158 | ?assertEqual(5, length(pool_call(Pid, get_all_workers))), 159 | checkin_worker(Pid, G), 160 | ?assertEqual(5, queue:len(pool_call(Pid, get_avail_workers))), 161 | ?assertEqual(5, length(pool_call(Pid, get_all_workers))), 162 | ?assertEqual(0, length(pool_call(Pid, get_all_monitors))), 163 | ok = pool_call(Pid, stop). 164 | 165 | pool_empty() -> 166 | %% Checks that the the pool handles the empty condition correctly when 167 | %% overflow is enabled. 168 | {ok, Pid} = new_pool(5, 2), 169 | Workers = [poolboy:checkout(Pid) || _ <- lists:seq(0, 6)], 170 | ?assertEqual(0, queue:len(pool_call(Pid, get_avail_workers))), 171 | ?assertEqual(7, length(pool_call(Pid, get_all_workers))), 172 | [A, B, C, D, E, F, G] = Workers, 173 | Self = self(), 174 | spawn(fun() -> 175 | Worker = poolboy:checkout(Pid), 176 | Self ! got_worker, 177 | checkin_worker(Pid, Worker) 178 | end), 179 | 180 | %% Spawned process should block waiting for worker to be available. 181 | receive 182 | got_worker -> ?assert(false) 183 | after 184 | 500 -> ?assert(true) 185 | end, 186 | checkin_worker(Pid, A), 187 | checkin_worker(Pid, B), 188 | 189 | %% Spawned process should have been able to obtain a worker. 190 | receive 191 | got_worker -> ?assert(true) 192 | after 193 | 500 -> ?assert(false) 194 | end, 195 | ?assertEqual(0, queue:len(pool_call(Pid, get_avail_workers))), 196 | ?assertEqual(5, length(pool_call(Pid, get_all_workers))), 197 | checkin_worker(Pid, C), 198 | checkin_worker(Pid, D), 199 | ?assertEqual(2, queue:len(pool_call(Pid, get_avail_workers))), 200 | ?assertEqual(5, length(pool_call(Pid, get_all_workers))), 201 | checkin_worker(Pid, E), 202 | checkin_worker(Pid, F), 203 | ?assertEqual(4, queue:len(pool_call(Pid, get_avail_workers))), 204 | ?assertEqual(5, length(pool_call(Pid, get_all_workers))), 205 | checkin_worker(Pid, G), 206 | ?assertEqual(5, queue:len(pool_call(Pid, get_avail_workers))), 207 | ?assertEqual(5, length(pool_call(Pid, get_all_workers))), 208 | ?assertEqual(0, length(pool_call(Pid, get_all_monitors))), 209 | ok = pool_call(Pid, stop). 210 | 211 | pool_empty_no_overflow() -> 212 | %% Checks the pool handles the empty condition properly when overflow is 213 | %% disabled. 214 | {ok, Pid} = new_pool(5, 0), 215 | Workers = [poolboy:checkout(Pid) || _ <- lists:seq(0, 4)], 216 | ?assertEqual(0, queue:len(pool_call(Pid, get_avail_workers))), 217 | ?assertEqual(5, length(pool_call(Pid, get_all_workers))), 218 | [A, B, C, D, E] = Workers, 219 | Self = self(), 220 | spawn(fun() -> 221 | Worker = poolboy:checkout(Pid), 222 | Self ! got_worker, 223 | checkin_worker(Pid, Worker) 224 | end), 225 | 226 | %% Spawned process should block waiting for worker to be available. 227 | receive 228 | got_worker -> ?assert(false) 229 | after 230 | 500 -> ?assert(true) 231 | end, 232 | checkin_worker(Pid, A), 233 | checkin_worker(Pid, B), 234 | 235 | %% Spawned process should have been able to obtain a worker. 236 | receive 237 | got_worker -> ?assert(true) 238 | after 239 | 500 -> ?assert(false) 240 | end, 241 | ?assertEqual(2, queue:len(pool_call(Pid, get_avail_workers))), 242 | ?assertEqual(5, length(pool_call(Pid, get_all_workers))), 243 | checkin_worker(Pid, C), 244 | checkin_worker(Pid, D), 245 | ?assertEqual(4, queue:len(pool_call(Pid, get_avail_workers))), 246 | ?assertEqual(5, length(pool_call(Pid, get_all_workers))), 247 | checkin_worker(Pid, E), 248 | ?assertEqual(5, queue:len(pool_call(Pid, get_avail_workers))), 249 | ?assertEqual(5, length(pool_call(Pid, get_all_workers))), 250 | ?assertEqual(0, length(pool_call(Pid, get_all_monitors))), 251 | ok = pool_call(Pid, stop). 252 | 253 | worker_death() -> 254 | %% Check that dead workers are only restarted when the pool is not full 255 | %% and the overflow count is 0. Meaning, don't restart overflow workers. 256 | {ok, Pid} = new_pool(5, 2), 257 | Worker = poolboy:checkout(Pid), 258 | kill_worker(Worker), 259 | ?assertEqual(5, queue:len(pool_call(Pid, get_avail_workers))), 260 | [A, B, C|_Workers] = [poolboy:checkout(Pid) || _ <- lists:seq(0, 6)], 261 | ?assertEqual(0, queue:len(pool_call(Pid, get_avail_workers))), 262 | ?assertEqual(7, length(pool_call(Pid, get_all_workers))), 263 | kill_worker(A), 264 | ?assertEqual(0, queue:len(pool_call(Pid, get_avail_workers))), 265 | ?assertEqual(6, length(pool_call(Pid, get_all_workers))), 266 | kill_worker(B), 267 | kill_worker(C), 268 | ?assertEqual(1, queue:len(pool_call(Pid, get_avail_workers))), 269 | ?assertEqual(5, length(pool_call(Pid, get_all_workers))), 270 | ?assertEqual(4, length(pool_call(Pid, get_all_monitors))), 271 | ok = pool_call(Pid, stop). 272 | 273 | worker_death_while_full() -> 274 | %% Check that if a worker dies while the pool is full and there is a 275 | %% queued checkout, a new worker is started and the checkout serviced. 276 | %% If there are no queued checkouts, a new worker is not started. 277 | {ok, Pid} = new_pool(5, 2), 278 | Worker = poolboy:checkout(Pid), 279 | kill_worker(Worker), 280 | ?assertEqual(5, queue:len(pool_call(Pid, get_avail_workers))), 281 | [A, B|_Workers] = [poolboy:checkout(Pid) || _ <- lists:seq(0, 6)], 282 | ?assertEqual(0, queue:len(pool_call(Pid, get_avail_workers))), 283 | ?assertEqual(7, length(pool_call(Pid, get_all_workers))), 284 | Self = self(), 285 | spawn(fun() -> 286 | poolboy:checkout(Pid), 287 | Self ! got_worker, 288 | %% XXX: Don't release the worker. We want to also test what happens 289 | %% when the worker pool is full and a worker dies with no queued 290 | %% checkouts. 291 | timer:sleep(5000) 292 | end), 293 | 294 | %% Spawned process should block waiting for worker to be available. 295 | receive 296 | got_worker -> ?assert(false) 297 | after 298 | 500 -> ?assert(true) 299 | end, 300 | kill_worker(A), 301 | 302 | %% Spawned process should have been able to obtain a worker. 303 | receive 304 | got_worker -> ?assert(true) 305 | after 306 | 1000 -> ?assert(false) 307 | end, 308 | kill_worker(B), 309 | ?assertEqual(0, queue:len(pool_call(Pid, get_avail_workers))), 310 | ?assertEqual(6, length(pool_call(Pid, get_all_workers))), 311 | ?assertEqual(6, length(pool_call(Pid, get_all_monitors))), 312 | ok = pool_call(Pid, stop). 313 | 314 | worker_death_while_full_no_overflow() -> 315 | %% Check that if a worker dies while the pool is full and there's no 316 | %% overflow, a new worker is started unconditionally and any queued 317 | %% checkouts are serviced. 318 | {ok, Pid} = new_pool(5, 0), 319 | Worker = poolboy:checkout(Pid), 320 | kill_worker(Worker), 321 | ?assertEqual(5, queue:len(pool_call(Pid, get_avail_workers))), 322 | [A, B, C|_Workers] = [poolboy:checkout(Pid) || _ <- lists:seq(0, 4)], 323 | ?assertEqual(0, queue:len(pool_call(Pid, get_avail_workers))), 324 | ?assertEqual(5, length(pool_call(Pid, get_all_workers))), 325 | Self = self(), 326 | spawn(fun() -> 327 | poolboy:checkout(Pid), 328 | Self ! got_worker, 329 | %% XXX: Do not release, need to also test when worker dies and no 330 | %% checkouts queued. 331 | timer:sleep(5000) 332 | end), 333 | 334 | %% Spawned process should block waiting for worker to be available. 335 | receive 336 | got_worker -> ?assert(false) 337 | after 338 | 500 -> ?assert(true) 339 | end, 340 | kill_worker(A), 341 | 342 | %% Spawned process should have been able to obtain a worker. 343 | receive 344 | got_worker -> ?assert(true) 345 | after 346 | 1000 -> ?assert(false) 347 | end, 348 | kill_worker(B), 349 | ?assertEqual(1, queue:len(pool_call(Pid, get_avail_workers))), 350 | ?assertEqual(5, length(pool_call(Pid, get_all_workers))), 351 | kill_worker(C), 352 | ?assertEqual(2, queue:len(pool_call(Pid, get_avail_workers))), 353 | ?assertEqual(5, length(pool_call(Pid, get_all_workers))), 354 | ?assertEqual(3, length(pool_call(Pid, get_all_monitors))), 355 | ok = pool_call(Pid, stop). 356 | 357 | pool_full_nonblocking_no_overflow() -> 358 | %% Check that when the pool is full, checkouts return 'full' when the 359 | %% option to use non-blocking checkouts is used. 360 | {ok, Pid} = new_pool(5, 0), 361 | Workers = [poolboy:checkout(Pid) || _ <- lists:seq(0, 4)], 362 | ?assertEqual(0, queue:len(pool_call(Pid, get_avail_workers))), 363 | ?assertEqual(5, length(pool_call(Pid, get_all_workers))), 364 | ?assertEqual(full, poolboy:checkout(Pid, false)), 365 | ?assertEqual(full, poolboy:checkout(Pid, false)), 366 | A = hd(Workers), 367 | checkin_worker(Pid, A), 368 | ?assertEqual(A, poolboy:checkout(Pid)), 369 | ?assertEqual(5, length(pool_call(Pid, get_all_monitors))), 370 | ok = pool_call(Pid, stop). 371 | 372 | pool_full_nonblocking() -> 373 | %% Check that when the pool is full, checkouts return 'full' when the 374 | %% option to use non-blocking checkouts is used. 375 | {ok, Pid} = new_pool(5, 5), 376 | Workers = [poolboy:checkout(Pid) || _ <- lists:seq(0, 9)], 377 | ?assertEqual(0, queue:len(pool_call(Pid, get_avail_workers))), 378 | ?assertEqual(10, length(pool_call(Pid, get_all_workers))), 379 | ?assertEqual(full, poolboy:checkout(Pid, false)), 380 | A = hd(Workers), 381 | checkin_worker(Pid, A), 382 | NewWorker = poolboy:checkout(Pid, false), 383 | ?assertEqual(false, is_process_alive(A)), %% Overflow workers get shutdown 384 | ?assert(is_pid(NewWorker)), 385 | ?assertEqual(full, poolboy:checkout(Pid, false)), 386 | ?assertEqual(10, length(pool_call(Pid, get_all_monitors))), 387 | ok = pool_call(Pid, stop). 388 | 389 | owner_death() -> 390 | %% Check that a dead owner (a process that dies with a worker checked out) 391 | %% causes the pool to dismiss the worker and prune the state space. 392 | {ok, Pid} = new_pool(5, 5), 393 | spawn(fun() -> 394 | poolboy:checkout(Pid), 395 | receive after 500 -> exit(normal) end 396 | end), 397 | timer:sleep(1000), 398 | ?assertEqual(5, queue:len(pool_call(Pid, get_avail_workers))), 399 | ?assertEqual(5, length(pool_call(Pid, get_all_workers))), 400 | ?assertEqual(0, length(pool_call(Pid, get_all_monitors))), 401 | ok = pool_call(Pid, stop). 402 | 403 | checkin_after_exception_in_transaction() -> 404 | {ok, Pool} = new_pool(2, 0), 405 | ?assertEqual(2, queue:len(pool_call(Pool, get_avail_workers))), 406 | Tx = fun(Worker) -> 407 | ?assert(is_pid(Worker)), 408 | ?assertEqual(1, queue:len(pool_call(Pool, get_avail_workers))), 409 | throw(it_on_the_ground), 410 | ?assert(false) 411 | end, 412 | try 413 | poolboy:transaction(Pool, Tx) 414 | catch 415 | throw:it_on_the_ground -> ok 416 | end, 417 | ?assertEqual(2, queue:len(pool_call(Pool, get_avail_workers))), 418 | ok = pool_call(Pool, stop). 419 | 420 | pool_returns_status() -> 421 | {ok, Pool} = new_pool(2, 0), 422 | ?assertEqual({ready, 2, 0, 0}, poolboy:status(Pool)), 423 | poolboy:checkout(Pool), 424 | ?assertEqual({ready, 1, 0, 1}, poolboy:status(Pool)), 425 | poolboy:checkout(Pool), 426 | ?assertEqual({full, 0, 0, 2}, poolboy:status(Pool)), 427 | ok = pool_call(Pool, stop), 428 | 429 | {ok, Pool2} = new_pool(1, 1), 430 | ?assertEqual({ready, 1, 0, 0}, poolboy:status(Pool2)), 431 | poolboy:checkout(Pool2), 432 | ?assertEqual({overflow, 0, 0, 1}, poolboy:status(Pool2)), 433 | poolboy:checkout(Pool2), 434 | ?assertEqual({full, 0, 1, 2}, poolboy:status(Pool2)), 435 | ok = pool_call(Pool2, stop), 436 | 437 | {ok, Pool3} = new_pool(0, 2), 438 | ?assertEqual({overflow, 0, 0, 0}, poolboy:status(Pool3)), 439 | poolboy:checkout(Pool3), 440 | ?assertEqual({overflow, 0, 1, 1}, poolboy:status(Pool3)), 441 | poolboy:checkout(Pool3), 442 | ?assertEqual({full, 0, 2, 2}, poolboy:status(Pool3)), 443 | ok = pool_call(Pool3, stop), 444 | 445 | {ok, Pool4} = new_pool(0, 0), 446 | ?assertEqual({full, 0, 0, 0}, poolboy:status(Pool4)), 447 | ok = pool_call(Pool4, stop). 448 | 449 | demonitors_previously_waiting_processes() -> 450 | {ok, Pool} = new_pool(1,0), 451 | Self = self(), 452 | Pid = spawn(fun() -> 453 | W = poolboy:checkout(Pool), 454 | Self ! ok, 455 | timer:sleep(500), 456 | poolboy:checkin(Pool, W), 457 | receive ok -> ok end 458 | end), 459 | receive ok -> ok end, 460 | Worker = poolboy:checkout(Pool), 461 | ?assertEqual(1, length(get_monitors(Pool))), 462 | poolboy:checkin(Pool, Worker), 463 | timer:sleep(500), 464 | ?assertEqual(0, length(get_monitors(Pool))), 465 | Pid ! ok, 466 | ok = pool_call(Pool, stop). 467 | 468 | demonitors_when_checkout_cancelled() -> 469 | {ok, Pool} = new_pool(1,0), 470 | Self = self(), 471 | Pid = spawn(fun() -> 472 | poolboy:checkout(Pool), 473 | _ = (catch poolboy:checkout(Pool, true, 1000)), 474 | Self ! ok, 475 | receive ok -> ok end 476 | end), 477 | timer:sleep(500), 478 | ?assertEqual(2, length(get_monitors(Pool))), 479 | receive ok -> ok end, 480 | ?assertEqual(1, length(get_monitors(Pool))), 481 | Pid ! ok, 482 | ok = pool_call(Pool, stop). 483 | 484 | default_strategy_lifo() -> 485 | %% Default strategy is LIFO 486 | {ok, Pid} = new_pool(2, 0), 487 | Worker1 = poolboy:checkout(Pid), 488 | ok = poolboy:checkin(Pid, Worker1), 489 | Worker1 = poolboy:checkout(Pid), 490 | poolboy:stop(Pid). 491 | 492 | lifo_strategy() -> 493 | {ok, Pid} = new_pool(2, 0, lifo), 494 | Worker1 = poolboy:checkout(Pid), 495 | ok = poolboy:checkin(Pid, Worker1), 496 | Worker1 = poolboy:checkout(Pid), 497 | poolboy:stop(Pid). 498 | 499 | fifo_strategy() -> 500 | {ok, Pid} = new_pool(2, 0, fifo), 501 | Worker1 = poolboy:checkout(Pid), 502 | ok = poolboy:checkin(Pid, Worker1), 503 | Worker2 = poolboy:checkout(Pid), 504 | ?assert(Worker1 =/= Worker2), 505 | Worker1 = poolboy:checkout(Pid), 506 | poolboy:stop(Pid). 507 | 508 | reuses_waiting_monitor_on_worker_exit() -> 509 | {ok, Pool} = new_pool(1,0), 510 | 511 | Self = self(), 512 | Pid = spawn(fun() -> 513 | Worker = poolboy:checkout(Pool), 514 | Self ! {worker, Worker}, 515 | poolboy:checkout(Pool), 516 | receive ok -> ok end 517 | end), 518 | 519 | Worker = receive {worker, Worker1} -> Worker1 end, 520 | Ref = monitor(process, Worker), 521 | exit(Worker, kill), 522 | receive 523 | {'DOWN', Ref, _, _, _} -> 524 | ok 525 | end, 526 | 527 | ?assertEqual(1, length(get_monitors(Pool))), 528 | 529 | Pid ! ok, 530 | ok = pool_call(Pool, stop). 531 | 532 | get_monitors(Pid) -> 533 | %% Synchronise with the Pid to ensure it has handled all expected work. 534 | _ = sys:get_status(Pid), 535 | [{monitors, Monitors}] = erlang:process_info(Pid, [monitors]), 536 | Monitors. 537 | 538 | new_pool(Size, MaxOverflow) -> 539 | poolboy:start_link([{name, {local, poolboy_test}}, 540 | {worker_module, poolboy_test_worker}, 541 | {size, Size}, {max_overflow, MaxOverflow}]). 542 | 543 | new_pool(Size, MaxOverflow, Strategy) -> 544 | poolboy:start_link([{name, {local, poolboy_test}}, 545 | {worker_module, poolboy_test_worker}, 546 | {size, Size}, {max_overflow, MaxOverflow}, 547 | {strategy, Strategy}]). 548 | 549 | pool_call(ServerRef, Request) -> 550 | gen_server:call(ServerRef, Request). 551 | --------------------------------------------------------------------------------