├── .gitignore ├── LICENSE ├── Makefile ├── README ├── ebin └── epgsql_pool.app ├── src ├── epgsql_pool.app ├── epgsql_pool.erl └── pgsql_pool.erl ├── test_ebin └── .empty └── test_src └── pgsql_pool_tests.erl /.gitignore: -------------------------------------------------------------------------------- 1 | \.beam 2 | \.boot 3 | \.script 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008, Will Glozer 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | * Neither the name of Will Glozer nor the names of his contributors may be used to endorse or promote products derived from this software without specific prior written permission. 9 | 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME := epgsql_pool 2 | VERSION := 0.1 3 | 4 | ERL := erl 5 | ERLC := erlc 6 | 7 | EPGSQL_EBIN := ~/src/epgsql/ebin 8 | 9 | # ------------------------------------------------------------------------ 10 | 11 | ERLC_FLAGS := -Wall 12 | 13 | SRC := $(wildcard src/*.erl) 14 | TESTS := $(wildcard test_src/*.erl) 15 | RELEASE := $(NAME)-$(VERSION).tar.gz 16 | 17 | APPDIR := $(NAME)-$(VERSION) 18 | BEAMS := $(SRC:src/%.erl=ebin/%.beam) 19 | 20 | compile: $(BEAMS) 21 | 22 | app: compile 23 | @mkdir -p $(APPDIR)/ebin 24 | @cp -r ebin/* $(APPDIR)/ebin/ 25 | 26 | release: app 27 | @tar czvf $(RELEASE) $(APPDIR) 28 | 29 | clean: 30 | @rm -f ebin/*.beam 31 | @rm -rf $(NAME)-$(VERSION) $(NAME)-*.tar.gz 32 | 33 | test: $(TESTS:test_src/%.erl=test_ebin/%.beam) $(BEAMS) 34 | $(ERL) -pa $(EPGSQL_EBIN) -pa ebin/ -pa test_ebin/ -noshell -s pgsql_pool_tests test -s init stop 35 | 36 | # ------------------------------------------------------------------------ 37 | 38 | .SUFFIXES: .erl .beam 39 | .PHONY: app compile clean test 40 | 41 | ebin/%.beam : src/%.erl 42 | $(ERLC) $(ERLC_FLAGS) -o $(dir $@) $< 43 | 44 | test_ebin/%.beam : test_src/%.erl 45 | $(ERLC) $(ERLC_FLAGS) -o $(dir $@) $< 46 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Erlang PostgreSQL Connection Pool 2 | 3 | * Application 4 | 5 | epgsql_pool will create any pools defined in the application's 'pools' environment 6 | parameter, which is a list of atoms. Each atom must refer to an environment parameter 7 | with the same name and value {Size, Opts} where Opts is a property list with the 8 | following supported options: 9 | 10 | host - host to connect to, default "localhost". 11 | port - port to connect to, default 5432. 12 | username - username to authenticate with, default os:getenv("USER"). 13 | password - password to authenticate with, default "". 14 | database - database to connect to, no default. 15 | 16 | .config file example: 17 | 18 | {epgsql_pool, [{pools, [db1, db2]}, 19 | {db1, {10, [{database, "db1"}]}}, 20 | {db2, {10, [{database, "db2"}]}}]} 21 | 22 | * Pool Usage 23 | 24 | {ok, C} = pgsql_pool:get_connection(Pool, Timeout). 25 | 26 | Pool - Pid or Name of pool. 27 | Timeout - Time, in milliseconds, to wait for a free connection. 28 | 29 | ok = pgsql_pool:return_connection(Pool, Connection). 30 | 31 | * Details 32 | 33 | epgsql_pool monitors the process which called get_connection and returns the 34 | allocated connection to the pool if that process dies. If a connection dies, 35 | a new one is created and added to the pool in its place. 36 | -------------------------------------------------------------------------------- /ebin/epgsql_pool.app: -------------------------------------------------------------------------------- 1 | ../src/epgsql_pool.app -------------------------------------------------------------------------------- /src/epgsql_pool.app: -------------------------------------------------------------------------------- 1 | {application, epgsql_pool, 2 | [{description, "PostgreSQL Connection Pool"}, 3 | {vsn, "0.1"}, 4 | {modules, [epgsql_pool, pgsql_pool]}, 5 | {registered, [epgsql_pool]}, 6 | {mod, {epgsql_pool, []}}, 7 | {applications, [kernel, stdlib, epgsql]}, 8 | {included_applications, []}, 9 | {env, [{pools, []}]}]}. 10 | -------------------------------------------------------------------------------- /src/epgsql_pool.erl: -------------------------------------------------------------------------------- 1 | -module(epgsql_pool). 2 | 3 | -behavior(application). 4 | -behavior(supervisor). 5 | 6 | -export([start_pool/3]). 7 | -export([start/2, stop/1, init/1]). 8 | 9 | %% -- client interface -- 10 | 11 | start_pool(Name, Size, Opts) -> 12 | supervisor:start_child(?MODULE, [Name, Size, Opts]). 13 | 14 | %% -- application implementation -- 15 | 16 | start(_Type, _Args) -> 17 | {ok, Pid} = supervisor:start_link({local, ?MODULE}, ?MODULE, []), 18 | {ok, Pools} = application:get_env(pools), 19 | case catch lists:foreach(fun start_pool/1, Pools) of 20 | {'EXIT', Why} -> {error, Why}; 21 | _Other -> {ok, Pid} 22 | end. 23 | 24 | stop(_State) -> 25 | ok. 26 | 27 | %% -- supervisor implementation -- 28 | 29 | init([]) -> 30 | {ok, 31 | {{simple_one_for_one, 2, 60}, 32 | [{pool, 33 | {pgsql_pool, start_link, []}, 34 | permanent, 2000, supervisor, 35 | [pgsql_pool]}]}}. 36 | 37 | %% -- internal functions -- 38 | 39 | start_pool(Name) -> 40 | case application:get_env(Name) of 41 | {ok, {Size, Opts}} -> start_pool(Name, Size, Opts); 42 | {ok, Value} -> exit({invalid_pool_spec, Value}); 43 | undefined -> exit({missing_pool_spec, Name}) 44 | end. 45 | -------------------------------------------------------------------------------- /src/pgsql_pool.erl: -------------------------------------------------------------------------------- 1 | -module(pgsql_pool). 2 | 3 | -export([start_link/2, start_link/3, stop/1]). 4 | -export([get_connection/1, get_connection/2, return_connection/2]). 5 | -export([get_database/1]). 6 | 7 | -export([init/1, code_change/3, terminate/2]). 8 | -export([handle_call/3, handle_cast/2, handle_info/2]). 9 | 10 | -record(state, {id, size, connections, monitors, waiting, opts, timer}). 11 | 12 | %% -- client interface -- 13 | 14 | opts(Opts) -> 15 | Defaults = [{host, "localhost"}, 16 | {port, 5432}, 17 | {password, ""}, 18 | {username, os:getenv("USER")}, 19 | {database, "not_given"}], 20 | Opts2 = lists:ukeysort(1, proplists:unfold(Opts)), 21 | proplists:normalize(lists:ukeymerge(1, Opts2, Defaults), []). 22 | 23 | 24 | start_link(Size, Opts) -> 25 | gen_server:start_link(?MODULE, {undefined, Size, opts(Opts)}, []). 26 | 27 | start_link(undefined, Size, Opts) -> 28 | start_link(Size, Opts); 29 | start_link(Name, Size, Opts) -> 30 | gen_server:start_link({local, Name}, ?MODULE, {Name, Size, opts(Opts)}, []). 31 | 32 | %% @doc Stop the pool, close all db connections 33 | stop(P) -> 34 | gen_server:cast(P, stop). 35 | 36 | %% @doc Get a db connection, wait at most 10 seconds before giving up. 37 | get_connection(P) -> 38 | get_connection(P, 10000). 39 | 40 | %% @doc Get a db connection, wait at most Timeout seconds before giving up. 41 | get_connection(P, Timeout) -> 42 | try 43 | gen_server:call(P, get_connection, Timeout) 44 | catch 45 | _:_ -> 46 | gen_server:cast(P, {cancel_wait, self()}), 47 | {error, timeout} 48 | end. 49 | 50 | %% @doc Return a db connection back to the connection pool. 51 | return_connection(P, C) -> 52 | gen_server:cast(P, {return_connection, C}). 53 | 54 | %% @doc Return the name of the database used for the pool. 55 | get_database(P) -> 56 | {ok, C} = get_connection(P), 57 | {ok, Db} = pgsql_connection:database(C), 58 | return_connection(P, C), 59 | {ok, Db}. 60 | 61 | %% -- gen_server implementation -- 62 | 63 | init({Name, Size, Opts}) -> 64 | process_flag(trap_exit, true), 65 | Id = case Name of 66 | undefined -> self(); 67 | _Name -> Name 68 | end, 69 | {ok, Connection} = connect(Opts), 70 | {ok, TRef} = timer:send_interval(60000, close_unused), 71 | State = #state{ 72 | id = Id, 73 | size = Size, 74 | opts = Opts, 75 | connections = [{Connection, now_secs()}], 76 | monitors = [], 77 | waiting = queue:new(), 78 | timer = TRef}, 79 | {ok, State}. 80 | 81 | %% Requestor wants a connection. When available then immediately return, otherwise add to the waiting queue. 82 | handle_call(get_connection, From, #state{connections = Connections, waiting = Waiting} = State) -> 83 | case Connections of 84 | [{C,_} | T] -> 85 | % Return existing unused connection 86 | {noreply, deliver(From, C, State#state{connections = T})}; 87 | [] -> 88 | case length(State#state.monitors) < State#state.size of 89 | true -> 90 | % Allocate a new connection and return it. 91 | {ok, C} = connect(State#state.opts), 92 | {noreply, deliver(From, C, State)}; 93 | false -> 94 | % Reached max connections, let the requestor wait 95 | {noreply, State#state{waiting = queue:in(From, Waiting)}} 96 | end 97 | end; 98 | 99 | %% Trap unsupported calls 100 | handle_call(Request, _From, State) -> 101 | {stop, {unsupported_call, Request}, State}. 102 | 103 | %% Connection returned from the requestor, back into our pool. Demonitor the requestor. 104 | handle_cast({return_connection, C}, #state{monitors = Monitors} = State) -> 105 | case lists:keytake(C, 1, Monitors) of 106 | {value, {C, M}, Monitors2} -> 107 | erlang:demonitor(M), 108 | {noreply, return(C, State#state{monitors = Monitors2})}; 109 | false -> 110 | {noreply, State} 111 | end; 112 | 113 | %% Requestor gave up (timeout), remove from our waiting queue (if any). 114 | handle_cast({cancel_wait, Pid}, #state{waiting = Waiting} = State) -> 115 | Waiting2 = queue:filter(fun({QPid, _Tag}) -> QPid =/= Pid end, Waiting), 116 | {noreply, State#state{waiting = Waiting2}}; 117 | 118 | %% Stop the connections pool. 119 | handle_cast(stop, State) -> 120 | {stop, normal, State}; 121 | 122 | %% Trap unsupported casts 123 | handle_cast(Request, State) -> 124 | {stop, {unsupported_cast, Request}, State}. 125 | 126 | %% Close all connections that are unused for longer than a minute. 127 | handle_info(close_unused, State) -> 128 | Old = now_secs() - 60, 129 | {Unused, Used} = lists:partition(fun({_C,Time}) -> Time < Old end, State#state.connections), 130 | [ pgsql:close(C) || {C,_} <- Unused ], 131 | {noreply, State#state{connections=Used}}; 132 | 133 | %% Requestor we are monitoring went down. Kill the associated connection, as it might be in an unknown state. 134 | handle_info({'DOWN', M, process, _Pid, _Info}, #state{monitors = Monitors} = State) -> 135 | case lists:keytake(M, 2, Monitors) of 136 | {value, {C, M}, Monitors2} -> 137 | pgsql:close(C), 138 | {noreply, State#state{monitors = Monitors2}}; 139 | false -> 140 | {noreply, State} 141 | end; 142 | 143 | %% One of our database connections went down. Clean up our administration. 144 | handle_info({'EXIT', ConnectionPid, _Reason}, State) -> 145 | #state{connections = Connections, monitors = Monitors} = State, 146 | Connections2 = proplists:delete(ConnectionPid, Connections), 147 | F = fun({C, M}) when C == ConnectionPid -> erlang:demonitor(M), false; 148 | ({_, _}) -> true 149 | end, 150 | Monitors2 = lists:filter(F, Monitors), 151 | {noreply, State#state{connections = Connections2, monitors = Monitors2}}; 152 | 153 | %% Trap unsupported info calls. 154 | handle_info(Info, State) -> 155 | {stop, {unsupported_info, Info}, State}. 156 | 157 | terminate(_Reason, State) -> 158 | timer:cancel(State#state.timer), 159 | ok. 160 | 161 | code_change(_OldVsn, State, _Extra) -> 162 | State. 163 | 164 | %% -- internal functions -- 165 | 166 | connect(Opts) -> 167 | Host = proplists:get_value(host, Opts), 168 | Username = proplists:get_value(username, Opts), 169 | Password = proplists:get_value(password, Opts), 170 | pgsql:connect(Host, Username, Password, Opts). 171 | 172 | deliver({Pid,_Tag} = From, C, #state{monitors=Monitors} = State) -> 173 | M = erlang:monitor(process, Pid), 174 | gen_server:reply(From, {ok, C}), 175 | State#state{ monitors=[{C, M} | Monitors] }. 176 | 177 | return(C, #state{connections = Connections, waiting = Waiting} = State) -> 178 | case queue:out(Waiting) of 179 | {{value, From}, Waiting2} -> 180 | State2 = deliver(From, C, State), 181 | State2#state{waiting = Waiting2}; 182 | {empty, _Waiting} -> 183 | Connections2 = [{C, now_secs()} | Connections], 184 | State#state{connections = Connections2} 185 | end. 186 | 187 | 188 | %% Return the current time in seconds, used for timeouts. 189 | now_secs() -> 190 | {M,S,_M} = erlang:now(), 191 | M*1000 + S. 192 | -------------------------------------------------------------------------------- /test_ebin/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephwecker/epgsql_pool/f283697eabc3c0952306a748f0c3a86b7fc1ded5/test_ebin/.empty -------------------------------------------------------------------------------- /test_src/pgsql_pool_tests.erl: -------------------------------------------------------------------------------- 1 | -module(pgsql_pool_tests). 2 | 3 | -include_lib("eunit/include/eunit.hrl"). 4 | 5 | -define(host, "localhost"). 6 | 7 | get_connections_test() -> 8 | with_pool_2( 9 | fun(P) -> 10 | {ok, C1} = get_connection(P), 11 | {ok, C2} = get_connection(P), 12 | ?assert(C1 =/= C2), 13 | ok = pgsql_pool:return_connection(P, C1), 14 | ok = pgsql_pool:return_connection(P, C2) 15 | end). 16 | 17 | get_connection_timeout_test() -> 18 | with_pool_0( 19 | fun(P) -> 20 | {error, timeout} = pgsql_pool:get_connection(P, 100) 21 | end). 22 | 23 | get_returned_connection_test() -> 24 | with_pool_1( 25 | fun(P) -> 26 | {ok, C} = get_connection(P), 27 | {error, timeout} = get_connection(P), 28 | ok = pgsql_pool:return_connection(P, C), 29 | {ok, C} = get_connection(P), 30 | ok = pgsql_pool:return_connection(P, C) 31 | end). 32 | 33 | return_twice_test() -> 34 | with_pool_1( 35 | fun(P) -> 36 | {ok, C} = get_connection(P), 37 | ok = pgsql_pool:return_connection(P, C), 38 | ok = pgsql_pool:return_connection(P, C) 39 | end). 40 | 41 | connection_dies_test() -> 42 | with_pool_1( 43 | fun(P) -> 44 | {ok, C1} = get_connection(P), 45 | exit(C1, kill), 46 | {ok, C2} = get_connection(P), 47 | ?assert(C1 =/= C2) 48 | end). 49 | 50 | connection_owner_dies_test() -> 51 | with_pool_1( 52 | fun(P) -> 53 | Self = self(), 54 | spawn(fun() -> 55 | {ok, C} = get_connection(P), 56 | Self ! {connection, C} 57 | end), 58 | receive 59 | {connection, C} -> 60 | {ok, C} = get_connection(P), 61 | ok = pgsql_pool:return_connection(P, C) 62 | end 63 | end). 64 | 65 | named_pool_test() -> 66 | Name = test_pool, 67 | {ok, _P} = pgsql_pool:start_link(Name, 1, [{host, ?host}]), 68 | {ok, C} = get_connection(Name), 69 | ok = pgsql_pool:return_connection(Name, C), 70 | pgsql_pool:stop(Name). 71 | 72 | %% -- internal functions -- 73 | 74 | with_pool(Size, F) -> 75 | {ok, P} = pgsql_pool:start_link(Size, [{host, ?host}]), 76 | try F(P) 77 | after 78 | pgsql_pool:stop(P) 79 | end. 80 | 81 | with_pool_0(F) -> with_pool(0, F). 82 | with_pool_1(F) -> with_pool(1, F). 83 | with_pool_2(F) -> with_pool(2, F). 84 | 85 | get_connection(P) -> 86 | case pgsql_pool:get_connection(P, 1000) of 87 | {ok, C} -> 88 | ok = test_connection(C), 89 | {ok, C}; 90 | Error -> 91 | Error 92 | end. 93 | 94 | test_connection(C) -> 95 | {ok, [_Col], [{<<"1">>}]} = pgsql:squery(C, "select 1"), 96 | ok. 97 | --------------------------------------------------------------------------------