├── .gitignore ├── .travis.yml ├── LICENSE.md ├── Makefile ├── README.md ├── rebar.config ├── rebar.lock ├── src ├── pgpool.app.src ├── pgpool.erl ├── pgpool.hrl ├── pgpool_app.erl ├── pgpool_sup.erl └── pgpool_worker.erl └── test ├── pgpool-test.config ├── pgpool_SUITE.erl ├── pgpool_test_suite_helper.erl └── results └── .keep /.gitignore: -------------------------------------------------------------------------------- 1 | # vim 2 | .*.sw[a-z] 3 | *.un~ 4 | Session.vim 5 | 6 | # intellij 7 | .idea 8 | *.iml 9 | 10 | # OSX ignores 11 | .DS_Store 12 | .AppleDouble 13 | .LSOverride 14 | Icon 15 | 16 | ._* 17 | .Spotlight-V100 18 | .Trashes 19 | .AppleDB 20 | .AppleDesktop 21 | Network Trash Folder 22 | Temporary Items 23 | .apdisk 24 | 25 | # project ignores 26 | _build 27 | erl_crash.dump 28 | *.beam 29 | test/results/* 30 | !test/results/.keep 31 | 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | 3 | otp_release: 4 | - 22.1 5 | - 21.3 6 | - 20.1 7 | - 19.3 8 | - 18.3 9 | 10 | branches: 11 | only: 12 | - master 13 | 14 | services: 15 | - postgresql 16 | 17 | before_install: 18 | - wget https://s3.amazonaws.com/rebar3/rebar3 19 | - chmod +x rebar3 20 | 21 | before_script: 22 | - psql -c 'create database pgpool_test;' -U postgres 23 | 24 | script: "make travis" 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2019 Roberto Ostinelli and Neato Robotics, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT_DIR:=$(strip $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))) 2 | 3 | all: 4 | @rebar3 compile 5 | 6 | clean: 7 | @rebar3 clean 8 | @find $(PROJECT_DIR)/. -name "erl_crash\.dump" | xargs rm -f 9 | @find $(PROJECT_DIR)/. -name "*\.beam" | xargs rm -f 10 | @find $(PROJECT_DIR)/. -name "*\.so" | xargs rm -f 11 | 12 | dialyzer: 13 | @rebar3 dialyzer 14 | 15 | test: all 16 | ct_run -dir $(PROJECT_DIR)/test -logdir $(PROJECT_DIR)/test/results \ 17 | -pa `rebar3 path` 18 | 19 | travis: 20 | @$(PROJECT_DIR)/rebar3 compile 21 | ct_run -dir $(PROJECT_DIR)/test -logdir $(PROJECT_DIR)/test/results \ 22 | -pa `$(PROJECT_DIR)/rebar3 path` 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![Build Status](https://travis-ci.org/ostinelli/pgpool.svg?branch=master)](https://travis-ci.org/ostinelli/pgpool) 3 | [![Hex pm](https://img.shields.io/hexpm/v/pgpool.svg)](https://hex.pm/packages/pgpool) 4 | 5 | # PGPool 6 | PGPool is a PosgreSQL client that automatically uses connection pools and handles reconnections in case of errors. PGPool also optimizes all of your statements, by preparing them and caching them for you under the hood. 7 | 8 | It uses: 9 | 10 | * [epgsql](https://github.com/epgsql/epgsql) as the PostgreSQL driver. 11 | * [poolboy](https://github.com/devinus/poolboy) as pooling library. 12 | 13 | 14 | ## Install 15 | 16 | ### For Elixir 17 | Add it to your deps: 18 | 19 | ```elixir 20 | defp deps do 21 | [{:pgpool, "~> 2.1"}] 22 | end 23 | ``` 24 | 25 | Ensure that `pgpool` is started with your application, for example by adding it to the list of your application's `extra_applications`: 26 | 27 | ```elixir 28 | def application do 29 | [ 30 | extra_applications: [:logger, :pgpool] 31 | ] 32 | end 33 | ``` 34 | 35 | ### For Erlang 36 | If you're using [rebar3](https://github.com/erlang/rebar3), add `pgpool` as a dependency in your project's `rebar.config` file: 37 | 38 | ```erlang 39 | {pgpool, {git, "git://github.com/ostinelli/pgpool.git", {tag, "2.1.0"}}} 40 | ``` 41 | 42 | Or, if you're using [Hex.pm](https://hex.pm/) as package manager (with the [rebar3_hex](https://github.com/hexpm/rebar3_hex) plugin): 43 | 44 | ```erlang 45 | {pgpool, "2.1.0"} 46 | ``` 47 | 48 | Ensure that `pgpool` is started with your application, for example by adding it in your `.app` file to the list of `applications`: 49 | 50 | ```erlang 51 | {application, my_app, [ 52 | %% ... 53 | {applications, [ 54 | kernel, 55 | stdlib, 56 | sasl, 57 | pgpool, 58 | %% ... 59 | ]}, 60 | %% ... 61 | ]}. 62 | ``` 63 | 64 | ## Usage 65 | Since `pgpool` is written in Erlang, the example code here below is in Erlang. Thanks to Elixir interoperability, the equivalent code in Elixir is straightforward. 66 | 67 | 68 | ### Specify Databases 69 | Databases can be set in the environment variable `pgpool`. You're probably best off using an application configuration file (in releases, `sys.config`): 70 | 71 | ```erlang 72 | {pgpool, [ 73 | {databases, [ 74 | {db1_name, [ 75 | {pool, [ 76 | %% poolboy options 77 | %% The `name` and `worker_module` options here will be ignored. 78 | {size, 10}, %% maximum pool size 79 | {max_overflow, 5}, %% maximum number of additional workers created if pool is empty 80 | {strategy, lifo} %% can be lifo or fifo (default is lifo) 81 | ]}, 82 | {connection, [ 83 | {host, "localhost"}, 84 | {user, "postgres"}, 85 | {pass, ""}, 86 | {options, [ 87 | %% epgsql connect_options() 88 | {port, 5432}, 89 | {ssl, false}, 90 | {database, "db1"} 91 | ]} 92 | ]} 93 | ]}, 94 | 95 | {db2_name, [ 96 | {pool, [ 97 | {size, 10}, 98 | {max_overflow, 20}, 99 | {strategy, lifo} 100 | ]}, 101 | {connection, [ 102 | {host, "localhost"}, 103 | {user, "postgres"}, 104 | {pass, ""}, 105 | {options, [ 106 | {port, 5432}, 107 | {ssl, false}, 108 | {database, "db2"} 109 | ]} 110 | ]} 111 | ]} 112 | ]} 113 | ]} 114 | ``` 115 | 116 | ### Custom types 117 | 118 | ```erlang 119 | -type pgpool_query_option() :: no_wait. 120 | ``` 121 | 122 | `no_wait` makes the query call return immediately if there are no available connections in the pool. This allows you to improve your application's flow control by rejecting external calls if your database is unable to handle the load, thus preventing a system overflow. 123 | 124 | ### Queries 125 | Please refer to [epgsql 3.4 README](https://github.com/epgsql/epgsql/blob/3.4.0/README.md) for how to perform queries. Currently, PGPool supports the following. 126 | 127 | #### Simple Query 128 | 129 | ```erlang 130 | pgpool:squery(DatabaseName, Sql) -> 131 | pgpool:squery(DatabaseName, Sql, []). 132 | ``` 133 | 134 | ```erlang 135 | pgpool:squery(DatabaseName, Sql) -> 136 | Result 137 | 138 | Types: 139 | DatabaseName = atom() 140 | Sql = string() | iodata() 141 | Options = [pgpool_query_option()] 142 | Result = {ok, Count} | {ok, Count, Rows} | {error, no_connection | no_available_connections} 143 | Count = non_neg_integer() 144 | Rows = (see epgsql for more details) 145 | ``` 146 | 147 | For example: 148 | 149 | ```erlang 150 | pgpool:squery(db1_name, "SELECT * FROM users;"). 151 | ``` 152 | 153 | > Simple queries cannot be optimized by PGPool since they cannot be prepared. If you want to optimize and cache your queries, consider using `equery/3,4` or `batch/2` instead. 154 | 155 | #### Extended Query 156 | 157 | ```erlang 158 | pgpool:equery(DatabaseName, Statement, Params) -> 159 | pgpool:equery(DatabaseName, Statement, Params, []). 160 | ``` 161 | 162 | ```erlang 163 | pgpool:equery(DatabaseName, Statement, Params, Options) -> 164 | Result 165 | 166 | Types: 167 | DatabaseName = atom() 168 | Statement = string() 169 | Params = list() 170 | Options = [pgpool_query_option()] 171 | Result = {ok, Count} | {ok, Count, Rows} | {error, no_connection | no_available_connections} 172 | Count = non_neg_integer() 173 | Rows = (see epgsql for more details) 174 | ``` 175 | 176 | For example: 177 | 178 | ```erlang 179 | pgpool:equery(db1_name, "SELECT * FROM users WHERE id = $1;", [3]). 180 | ``` 181 | 182 | > PGPool will prepare your statements and cache them for you, which results in considerable speed improvements. If you use a lot of different statements, consider memory usage because the statements are not garbage collected. 183 | 184 | #### Batch Queries 185 | To execute a batch: 186 | 187 | ```erlang 188 | pgpool:batch(DatabaseName, StatementsWithParams) -> 189 | pgpool:batch(DatabaseName, StatementsWithParams, Options). 190 | ``` 191 | 192 | ```erlang 193 | pgpool:batch(DatabaseName, StatementsWithParams) -> 194 | Result 195 | 196 | Types: 197 | DatabaseName = atom() 198 | StatementsWithParams = [{Statement, Params}] 199 | Statement = string() 200 | Params = list() 201 | Options = [pgpool_query_option()] 202 | Result = [{ok, Count} | {ok, Count, Rows}] | {error, no_connection | no_available_connections} 203 | Count = non_neg_integer() 204 | Rows = (see epgsql for more details) 205 | ``` 206 | 207 | For example: 208 | 209 | ```erlang 210 | S = "INSERT INTO users (name) VALUES ($1);", 211 | 212 | [{ok, 1}, {ok, 1}] = pgpool:batch(db1_name, [ 213 | {S, ["Hedy"]}, 214 | {S, ["Roberto"]} 215 | ]). 216 | ``` 217 | 218 | > PGPool will prepare your statements and cache them for you, which results in considerable speed improvements. If you use a lot of different statements, consider memory usage because the statements are not garbage collected. 219 | 220 | 221 | ## Contributing 222 | So you want to contribute? That's great! Please follow the guidelines below. It will make it easier to get merged in. 223 | 224 | Before implementing a new feature, please submit a ticket to discuss what you intend to do. Your feature might already be in the works, or an alternative implementation might have already been discussed. 225 | 226 | Do not commit to master in your fork. Provide a clean branch without merge commits. Every pull request should have its own topic branch. In this way, every additional adjustments to the original pull request might be done easily, and squashed with `git rebase -i`. The updated branch will be visible in the same pull request, so there will be no need to open new pull requests when there are changes to be applied. 227 | 228 | Ensure that proper testing is included. To run PGPool tests, you need to create the database `pgpool_test` for user `postgres` with no password, and then simply run from the project's root directory: 229 | 230 | ``` 231 | $ make test 232 | ``` 233 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {deps, [ 2 | {epgsql, "3.4.0"}, 3 | {poolboy, "1.5.2"} 4 | ]}. 5 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.1.0", 2 | [{<<"epgsql">>,{pkg,<<"epgsql">>,<<"3.4.0">>},0}, 3 | {<<"poolboy">>,{pkg,<<"poolboy">>,<<"1.5.2">>},0}]}. 4 | [ 5 | {pkg_hash,[ 6 | {<<"epgsql">>, <<"39D473B83D74329A88F8FB1F99AD24C7A2329FD753957C488808CB7C0FC8164E">>}, 7 | {<<"poolboy">>, <<"392B007A1693A64540CEAD79830443ABF5762F5D30CF50BC95CB2C1AAAFA006B">>}]} 8 | ]. 9 | -------------------------------------------------------------------------------- /src/pgpool.app.src: -------------------------------------------------------------------------------- 1 | {application, pgpool, 2 | [ 3 | {description, "A PosgreSQL client that automatically uses connection pools and reconnects in case of errors."}, 4 | {vsn, "2.1.0"}, 5 | {registered, [ 6 | pgpool_sup 7 | ]}, 8 | {applications, [ 9 | kernel, 10 | stdlib, 11 | poolboy, 12 | asn1, 13 | crypto, 14 | public_key, 15 | ssl, 16 | epgsql 17 | ]}, 18 | {mod, {pgpool_app, []}}, 19 | {env, []}, 20 | 21 | {licenses, ["MIT"]}, 22 | {links, [{"Github", "https://github.com/ostinelli/pgpool"}]} 23 | ] 24 | }. 25 | -------------------------------------------------------------------------------- /src/pgpool.erl: -------------------------------------------------------------------------------- 1 | %% ========================================================================================================== 2 | %% PGPool - A PosgreSQL client that automatically uses connection pools and reconnects in case of errors. 3 | %% 4 | %% The MIT License (MIT) 5 | %% 6 | %% Copyright (c) 2016-2019 Roberto Ostinelli and Neato Robotics, Inc. 7 | %% 8 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 9 | %% of this software and associated documentation files (the "Software"), to deal 10 | %% in the Software without restriction, including without limitation the rights 11 | %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | %% copies of the Software, and to permit persons to whom the Software is 13 | %% furnished to do so, subject to the following conditions: 14 | %% 15 | %% The above copyright notice and this permission notice shall be included in 16 | %% all copies or substantial portions of the Software. 17 | %% 18 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | %% THE SOFTWARE. 25 | %% ========================================================================================================== 26 | -module(pgpool). 27 | 28 | %% API 29 | -export([start/0, stop/0]). 30 | -export([squery/2, squery/3]). 31 | -export([equery/3, equery/4]). 32 | -export([batch/2, batch/3]). 33 | 34 | %% includes 35 | -include("pgpool.hrl"). 36 | 37 | %% =================================================================== 38 | %% API 39 | %% =================================================================== 40 | -spec start() -> ok. 41 | start() -> 42 | ok = ensure_started(poolboy), 43 | ok = ensure_started(asn1), 44 | ok = ensure_started(crypto), 45 | ok = ensure_started(public_key), 46 | ok = ensure_started(ssl), 47 | ok = ensure_started(epgsql), 48 | ok = ensure_started(pgpool). 49 | 50 | -spec stop() -> ok. 51 | stop() -> 52 | ok = application:stop(pgpool). 53 | 54 | -spec squery(DatabaseName :: atom(), Sql :: string() | iodata()) -> 55 | {ok, Count :: non_neg_integer()} 56 | | {ok, Count :: non_neg_integer(), Rows :: any()} 57 | | {error, no_connection | no_available_connections}. 58 | squery(DatabaseName, Sql) -> 59 | pgpool_worker:squery(DatabaseName, Sql). 60 | 61 | -spec squery(DatabaseName :: atom(), Sql :: string() | iodata(), Options :: [pgpool_query_option()]) -> 62 | {ok, Count :: non_neg_integer()} 63 | | {ok, Count :: non_neg_integer(), Rows :: any()} 64 | | {error, no_connection | no_available_connections}. 65 | squery(DatabaseName, Sql, Options) -> 66 | pgpool_worker:squery(DatabaseName, Sql, Options). 67 | 68 | -spec equery(DatabaseName :: atom(), Statement :: string(), Params :: list()) -> any(). 69 | equery(DatabaseName, Statement, Params) -> 70 | pgpool_worker:equery(DatabaseName, Statement, Params). 71 | 72 | -spec equery(DatabaseName :: atom(), Statement :: string(), Params :: list(), Options :: [pgpool_query_option()]) -> 73 | any() | {error, no_available_connections}. 74 | equery(DatabaseName, Statement, Params, Options) -> 75 | pgpool_worker:equery(DatabaseName, Statement, Params, Options). 76 | 77 | -spec batch(DatabaseName :: atom(), [{Statement :: string(), Params :: list()}]) -> 78 | [{ok, Count :: non_neg_integer()} | {ok, Count :: non_neg_integer(), Rows :: any()}]. 79 | batch(DatabaseName, StatementsWithParams) -> 80 | pgpool_worker:batch(DatabaseName, StatementsWithParams). 81 | 82 | -spec batch(DatabaseName :: atom(), [{Statement :: string(), Params :: list()}], Options :: [pgpool_query_option()]) -> 83 | [{ok, Count :: non_neg_integer()} | {ok, Count :: non_neg_integer(), Rows :: any()}] | {error, no_available_connections}. 84 | batch(DatabaseName, StatementsWithParams, Options) -> 85 | pgpool_worker:batch(DatabaseName, StatementsWithParams, Options). 86 | 87 | %% =================================================================== 88 | %% Internal 89 | %% =================================================================== 90 | -spec ensure_started(App :: atom()) -> ok. 91 | ensure_started(App) -> 92 | case application:start(App) of 93 | ok -> ok; 94 | {error, {already_started, App}} -> ok 95 | end. 96 | -------------------------------------------------------------------------------- /src/pgpool.hrl: -------------------------------------------------------------------------------- 1 | %% ========================================================================================================== 2 | %% PGPool - A PosgreSQL client that automatically uses connection pools and reconnects in case of errors. 3 | %% 4 | %% The MIT License (MIT) 5 | %% 6 | %% Copyright (c) 2016-2019 Roberto Ostinelli and Neato Robotics, Inc. 7 | %% 8 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 9 | %% of this software and associated documentation files (the "Software"), to deal 10 | %% in the Software without restriction, including without limitation the rights 11 | %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | %% copies of the Software, and to permit persons to whom the Software is 13 | %% furnished to do so, subject to the following conditions: 14 | %% 15 | %% The above copyright notice and this permission notice shall be included in 16 | %% all copies or substantial portions of the Software. 17 | %% 18 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | %% THE SOFTWARE. 25 | %% ========================================================================================================== 26 | 27 | %% types 28 | -type pgpool_query_option() :: no_wait. 29 | -------------------------------------------------------------------------------- /src/pgpool_app.erl: -------------------------------------------------------------------------------- 1 | %% ========================================================================================================== 2 | %% PGPool - A PosgreSQL client that automatically uses connection pools and reconnects in case of errors. 3 | %% 4 | %% The MIT License (MIT) 5 | %% 6 | %% Copyright (c) 2016-2019 Roberto Ostinelli and Neato Robotics, Inc. 7 | %% 8 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 9 | %% of this software and associated documentation files (the "Software"), to deal 10 | %% in the Software without restriction, including without limitation the rights 11 | %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | %% copies of the Software, and to permit persons to whom the Software is 13 | %% furnished to do so, subject to the following conditions: 14 | %% 15 | %% The above copyright notice and this permission notice shall be included in 16 | %% all copies or substantial portions of the Software. 17 | %% 18 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | %% THE SOFTWARE. 25 | %% ========================================================================================================== 26 | -module(pgpool_app). 27 | -behaviour(application). 28 | 29 | %% API 30 | -export([start/2, stop/1]). 31 | 32 | %% =================================================================== 33 | %% API 34 | %% =================================================================== 35 | -spec start( 36 | StartType :: normal | {takeover, node()} | {failover, node()}, 37 | StartArgs :: any() 38 | ) -> {ok, pid()} | {ok, pid(), State :: any()} | {error, any()}. 39 | start(_StartType, _StartArgs) -> 40 | %% start sup 41 | pgpool_sup:start_link(). 42 | 43 | -spec stop(State :: any()) -> ok. 44 | stop(_State) -> 45 | ok. 46 | -------------------------------------------------------------------------------- /src/pgpool_sup.erl: -------------------------------------------------------------------------------- 1 | %% ========================================================================================================== 2 | %% PGPool - A PosgreSQL client that automatically uses connection pools and reconnects in case of errors. 3 | %% 4 | %% The MIT License (MIT) 5 | %% 6 | %% Copyright (c) 2016-2019 Roberto Ostinelli and Neato Robotics, Inc. 7 | %% 8 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 9 | %% of this software and associated documentation files (the "Software"), to deal 10 | %% in the Software without restriction, including without limitation the rights 11 | %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | %% copies of the Software, and to permit persons to whom the Software is 13 | %% furnished to do so, subject to the following conditions: 14 | %% 15 | %% The above copyright notice and this permission notice shall be included in 16 | %% all copies or substantial portions of the Software. 17 | %% 18 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | %% THE SOFTWARE. 25 | %% ========================================================================================================== 26 | -module(pgpool_sup). 27 | -behaviour(supervisor). 28 | 29 | %% API 30 | -export([start_link/0]). 31 | 32 | %% supervisor callbacks 33 | -export([init/1]). 34 | 35 | %% =================================================================== 36 | %% API 37 | %% =================================================================== 38 | -spec start_link() -> {ok, pid()} | {already_started, pid()} | shutdown. 39 | start_link() -> 40 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 41 | 42 | %% =================================================================== 43 | %% Callbacks 44 | %% =================================================================== 45 | -spec init([]) -> 46 | {ok, {{supervisor:strategy(), non_neg_integer(), pos_integer()}, [supervisor:child_spec()]}}. 47 | init([]) -> 48 | %% get databases 49 | Children = case application:get_env(databases) of 50 | {ok, Databases} -> 51 | children_spec(Databases); 52 | _ -> 53 | [] 54 | end, 55 | %% start sup 56 | {ok, {{one_for_one, 10, 10}, Children}}. 57 | 58 | %% =================================================================== 59 | %% Internal 60 | %% =================================================================== 61 | -spec children_spec(Databases :: any()) -> [supervisor:child_spec()]. 62 | children_spec(Databases) -> 63 | children_spec(Databases, []). 64 | children_spec([], Specs) -> 65 | Specs; 66 | children_spec([{DatabaseName, DatabaseInfo} | T], Specs) -> 67 | PoolArgs0 = proplists:get_value(pool, DatabaseInfo), 68 | ConnectionArgs0 = proplists:get_value(connection, DatabaseInfo), 69 | 70 | PoolArgs = [ 71 | {name, {local, DatabaseName}}, 72 | {worker_module, pgpool_worker} 73 | ] ++ remove_list_elements([name, worker_module], PoolArgs0), 74 | 75 | ConnectionArgs = [ 76 | {database_name, DatabaseName} 77 | ] ++ remove_list_elements([database_name], ConnectionArgs0), 78 | 79 | Spec = poolboy:child_spec(DatabaseName, PoolArgs, ConnectionArgs), 80 | children_spec(T, [Spec | Specs]). 81 | 82 | remove_list_elements([], L) -> L; 83 | remove_list_elements([El|T], L) -> 84 | remove_list_elements(T, lists:keydelete(El, 1, L)). 85 | -------------------------------------------------------------------------------- /src/pgpool_worker.erl: -------------------------------------------------------------------------------- 1 | %% ========================================================================================================== 2 | %% PGPool - A PosgreSQL client that automatically uses connection pools and reconnects in case of errors. 3 | %% 4 | %% The MIT License (MIT) 5 | %% 6 | %% Copyright (c) 2016-2019 Roberto Ostinelli and Neato Robotics, Inc. 7 | %% 8 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 9 | %% of this software and associated documentation files (the "Software"), to deal 10 | %% in the Software without restriction, including without limitation the rights 11 | %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | %% copies of the Software, and to permit persons to whom the Software is 13 | %% furnished to do so, subject to the following conditions: 14 | %% 15 | %% The above copyright notice and this permission notice shall be included in 16 | %% all copies or substantial portions of the Software. 17 | %% 18 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | %% THE SOFTWARE. 25 | %% ========================================================================================================== 26 | -module(pgpool_worker). 27 | -behaviour(gen_server). 28 | -behaviour(poolboy_worker). 29 | 30 | %% API 31 | -export([start_link/1]). 32 | -export([squery/2, squery/3]). 33 | -export([equery/3, equery/4]). 34 | -export([batch/2, batch/3]). 35 | 36 | %% gen_server callbacks 37 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). 38 | 39 | %% records 40 | -record(state, { 41 | database_name = undefined :: undefined | atom(), 42 | host = undefined :: undefined | string(), 43 | user = undefined :: undefined | string(), 44 | pass = undefined :: undefined | string(), 45 | connect_options = undefined :: undefined | list(), 46 | conn = undefined :: undefined | any(), 47 | timer_ref = undefined :: undefined | reference(), 48 | prepared_statements = undefined :: any() 49 | }). 50 | 51 | %% macros 52 | -define(RECONNECT_TIMEOUT_MS, 5000). 53 | -define(RETRY_SLEEP_MS, 1000). 54 | 55 | %% includes 56 | -include("pgpool.hrl"). 57 | 58 | %% =================================================================== 59 | %% API 60 | %% =================================================================== 61 | -spec start_link(Args :: list()) -> {ok, pid()} | {error, any()}. 62 | start_link(Args) -> 63 | gen_server:start_link(?MODULE, Args, []). 64 | 65 | -spec squery(DatabaseName :: atom(), Sql :: string() | iodata()) -> 66 | {ok, Count :: non_neg_integer()} 67 | | {ok, Count :: non_neg_integer(), Rows :: any()} 68 | | {error, no_connection | no_available_connections}. 69 | squery(DatabaseName, Sql) -> 70 | squery(DatabaseName, Sql, []). 71 | 72 | -spec squery(DatabaseName :: atom(), Sql :: string() | iodata(), Options :: [pgpool_query_option()]) -> 73 | {ok, Count :: non_neg_integer()} 74 | | {ok, Count :: non_neg_integer(), Rows :: any()} 75 | | {error, no_connection | no_available_connections}. 76 | squery(DatabaseName, Sql, Options) -> 77 | transaction(DatabaseName, {squery, Sql}, Options). 78 | 79 | -spec equery(DatabaseName :: atom(), Statement :: string(), Params :: list()) -> 80 | {ok, Count :: non_neg_integer()} 81 | | {ok, Count :: non_neg_integer(), Rows :: any()} 82 | | {error, no_connection | no_available_connections}. 83 | equery(DatabaseName, Statement, Params) -> 84 | equery(DatabaseName, Statement, Params, []). 85 | 86 | -spec equery(DatabaseName :: atom(), Statement :: string(), Params :: list(), Options :: [pgpool_query_option()]) -> 87 | {ok, Count :: non_neg_integer()} 88 | | {ok, Count :: non_neg_integer(), Rows :: any()} 89 | | {error, no_connection | no_available_connections}. 90 | equery(DatabaseName, Statement, Params, Options) -> 91 | transaction(DatabaseName, {equery, Statement, Params}, Options). 92 | 93 | -spec batch(DatabaseName :: atom(), [{Statement :: string(), Params :: list()}]) -> 94 | [{ok, Count :: non_neg_integer()} | {ok, Count :: non_neg_integer(), Rows :: any()}] 95 | | {error, no_connection | no_available_connections}. 96 | batch(DatabaseName, StatementsWithParams) -> 97 | batch(DatabaseName, StatementsWithParams, []). 98 | 99 | -spec batch(DatabaseName :: atom(), [{Statement :: string(), Params :: list()}], Options :: [pgpool_query_option()]) -> 100 | [{ok, Count :: non_neg_integer()} | {ok, Count :: non_neg_integer(), Rows :: any()}] 101 | | {error, no_connection | no_available_connections}. 102 | batch(DatabaseName, StatementsWithParams, Options) -> 103 | transaction(DatabaseName, {batch, StatementsWithParams}, Options). 104 | 105 | %% =================================================================== 106 | %% Callbacks 107 | %% =================================================================== 108 | 109 | %% ---------------------------------------------------------------------------------------------------------- 110 | %% Init 111 | %% ---------------------------------------------------------------------------------------------------------- 112 | -spec init(Args :: list()) -> 113 | {ok, #state{}} | 114 | {ok, #state{}, Timeout :: non_neg_integer()} | 115 | ignore | 116 | {stop, Reason :: any()}. 117 | init(Args) -> 118 | process_flag(trap_exit, true), 119 | 120 | %% read options 121 | DatabaseName = proplists:get_value(database_name, Args), 122 | 123 | %% read connection options 124 | Host = proplists:get_value(host, Args), 125 | User = proplists:get_value(user, Args), 126 | Pass = proplists:get_value(pass, Args), 127 | ConnectOptions = proplists:get_value(options, Args), 128 | 129 | %% build state 130 | State = #state{ 131 | database_name = DatabaseName, 132 | host = Host, 133 | user = User, 134 | pass = Pass, 135 | connect_options = ConnectOptions, 136 | prepared_statements = dict:new() 137 | }, 138 | 139 | %% connect 140 | State1 = connect(State), 141 | 142 | %% return 143 | {ok, State1}. 144 | 145 | %% ---------------------------------------------------------------------------------------------------------- 146 | %% Call messages 147 | %% ---------------------------------------------------------------------------------------------------------- 148 | -spec handle_call(Request :: any(), From :: any(), #state{}) -> 149 | {reply, Reply :: any(), #state{}} | 150 | {reply, Reply :: any(), #state{}, Timeout :: non_neg_integer()} | 151 | {noreply, #state{}} | 152 | {noreply, #state{}, Timeout :: non_neg_integer()} | 153 | {stop, Reason :: any(), Reply :: any(), #state{}} | 154 | {stop, Reason :: any(), #state{}}. 155 | 156 | handle_call(_Msg, _From, #state{conn = undefined} = State) -> 157 | {reply, {error, no_connection}, State}; 158 | 159 | handle_call({squery, Sql}, _From, #state{conn = Conn} = State) -> 160 | {reply, epgsql:squery(Conn, Sql), State}; 161 | 162 | handle_call({equery, Statement, Params}, _From, #state{conn = Conn} = State) -> 163 | {_, Name, State1} = prepare_or_get_statement(Statement, State), 164 | {reply, epgsql:prepared_query(Conn, Name, Params), State1}; 165 | 166 | handle_call({batch, StatementsWithParams}, _From, #state{ 167 | conn = Conn 168 | } = State) -> 169 | %% prepare & cache statements 170 | F = fun({Statement, Params}, {PreparedStatementsAcc, StateAcc}) -> 171 | %% get or prepare 172 | {PreparedStatement, _, StateAcc1} = prepare_or_get_statement(Statement, StateAcc), 173 | %% acc 174 | {[{PreparedStatement, Params} | PreparedStatementsAcc], StateAcc1} 175 | end, 176 | {StatementsForBatchRev, State1} = lists:foldl(F, {[], State}, StatementsWithParams), 177 | StatementsForBatch = lists:reverse(StatementsForBatchRev), 178 | %% execute batch 179 | {reply, epgsql:execute_batch(Conn, StatementsForBatch), State1}; 180 | 181 | handle_call(Request, From, State) -> 182 | error_logger:warning_msg("Received from ~p an unknown call message: ~p", [Request, From]), 183 | {reply, undefined, State}. 184 | 185 | %% ---------------------------------------------------------------------------------------------------------- 186 | %% Cast messages 187 | %% ---------------------------------------------------------------------------------------------------------- 188 | -spec handle_cast(Msg :: any(), #state{}) -> 189 | {noreply, #state{}} | 190 | {noreply, #state{}, Timeout :: non_neg_integer()} | 191 | {stop, Reason :: any(), #state{}}. 192 | 193 | handle_cast(Msg, State) -> 194 | error_logger:warning_msg("Received an unknown cast message: ~p", [Msg]), 195 | {noreply, State}. 196 | 197 | %% ---------------------------------------------------------------------------------------------------------- 198 | %% All non Call / Cast messages 199 | %% ---------------------------------------------------------------------------------------------------------- 200 | -spec handle_info(Info :: any(), #state{}) -> 201 | {noreply, #state{}} | 202 | {noreply, #state{}, Timeout :: non_neg_integer()} | 203 | {stop, Reason :: any(), #state{}}. 204 | 205 | handle_info(connect, State) -> 206 | State1 = connect(State), 207 | {noreply, State1}; 208 | 209 | handle_info({'EXIT', _From, _Reason}, State) -> 210 | error_logger:error_msg("epgsql process died, start a timer to reconnect"), 211 | State1 = timeout(State), 212 | {noreply, State1}; 213 | 214 | handle_info(Info, State) -> 215 | error_logger:warning_msg("Received an unknown info message: ~p", [Info]), 216 | {noreply, State}. 217 | 218 | %% ---------------------------------------------------------------------------------------------------------- 219 | %% Terminate 220 | %% ---------------------------------------------------------------------------------------------------------- 221 | -spec terminate(Reason :: any(), #state{}) -> terminated. 222 | terminate(_Reason, #state{conn = Conn}) -> 223 | %% terminate 224 | case Conn of 225 | undefined -> ok; 226 | _ -> ok = epgsql:close(Conn) 227 | end, 228 | terminated. 229 | 230 | %% ---------------------------------------------------------------------------------------------------------- 231 | %% Convert process state when code is changed. 232 | %% ---------------------------------------------------------------------------------------------------------- 233 | -spec code_change(OldVsn :: any(), #state{}, Extra :: any()) -> {ok, #state{}}. 234 | code_change(_OldVsn, State, _Extra) -> 235 | {ok, State}. 236 | 237 | %% =================================================================== 238 | %% Internal 239 | %% =================================================================== 240 | -spec connect(#state{}) -> #state{}. 241 | connect(#state{ 242 | database_name = DatabaseName, 243 | host = Host, 244 | user = User, 245 | pass = Pass, 246 | connect_options = ConnectOptions 247 | } = State) -> 248 | error_logger:info_msg("Connecting to database ~p", [DatabaseName]), 249 | 250 | case epgsql:connect(Host, User, Pass, ConnectOptions) of 251 | {ok, Conn} -> 252 | State#state{conn = Conn, prepared_statements = dict:new()}; 253 | Error -> 254 | error_logger:error_msg("Error connecting to database ~p with host ~p, user ~p, options ~p: ~p, will try reconnecting in ~p ms", [ 255 | DatabaseName, Host, User, ConnectOptions, Error, ?RECONNECT_TIMEOUT_MS 256 | ]), 257 | State#state{conn = undefined} 258 | end. 259 | 260 | -spec timeout(#state{}) -> #state{}. 261 | timeout(#state{ 262 | timer_ref = TimerPrevRef 263 | } = State) -> 264 | case TimerPrevRef of 265 | undefined -> ignore; 266 | _ -> erlang:cancel_timer(TimerPrevRef) 267 | end, 268 | TimerRef = erlang:send_after(?RECONNECT_TIMEOUT_MS, self(), connect), 269 | State#state{timer_ref = TimerRef}. 270 | 271 | -spec prepare_or_get_statement(Statement :: string(), #state{}) -> {PreparedStatement :: any(), StatementName :: string(), #state{}}. 272 | prepare_or_get_statement(Statement, #state{ 273 | conn = Conn, 274 | prepared_statements = PreparedStatements 275 | } = State) -> 276 | Name = "statement_" ++ integer_to_list(erlang:phash2(Statement)), 277 | case dict:find(Name, PreparedStatements) of 278 | {ok, PreparedStatement} -> 279 | {PreparedStatement, Name, State}; 280 | error -> 281 | %% prepare statement 282 | {ok, PreparedStatement} = epgsql:parse(Conn, Name, Statement, []), 283 | %% store 284 | PreparedStatements1 = dict:store(Name, PreparedStatement, PreparedStatements), 285 | %% update state 286 | State1 = State#state{prepared_statements = PreparedStatements1}, 287 | %% return 288 | {PreparedStatement, Name, State1} 289 | end. 290 | 291 | -spec transaction( 292 | DatabaseName :: atom(), 293 | Message :: {squery, Sql :: string() | iodata()} 294 | | {equery, Statement :: string(), Params :: list()} 295 | | {batch, [{Statement :: string(), Params :: list()}]}, 296 | Options :: [pgpool_query_option()] 297 | ) -> 298 | {ok, Count :: non_neg_integer()} 299 | | {ok, Count :: non_neg_integer(), Rows :: any()} 300 | | [{ok, Count :: non_neg_integer()} | {ok, Count :: non_neg_integer(), Rows :: any()}] 301 | | {error, no_connection | no_available_connections}. 302 | transaction(DatabaseName, Message, Options) -> 303 | Block = not lists:member(no_wait, Options), 304 | case poolboy:checkout(DatabaseName, Block) of 305 | full -> 306 | {error, no_available_connections}; 307 | Worker -> 308 | try 309 | gen_server:call(Worker, Message) 310 | after 311 | ok = poolboy:checkin(DatabaseName, Worker) 312 | end 313 | end. 314 | -------------------------------------------------------------------------------- /test/pgpool-test.config: -------------------------------------------------------------------------------- 1 | %%%=================================================================== 2 | %%% PgPool - TEST CONFIGURATION FILE 3 | %%%=================================================================== 4 | 5 | [ 6 | 7 | %% PgPool config 8 | {pgpool, [ 9 | {databases, [ 10 | {pgpool_test, [ 11 | {pool, [ 12 | {size, 1}, 13 | {max_overflow, 0}, 14 | {strategy, lifo} 15 | ]}, 16 | {connection, [ 17 | {host, "localhost"}, 18 | {user, "postgres"}, 19 | {pass, ""}, 20 | {options, [ 21 | {port, 5432}, 22 | {ssl, false}, 23 | {database, "pgpool_test"} 24 | ]} 25 | ]} 26 | ]} 27 | ]} 28 | ]} 29 | 30 | ]. -------------------------------------------------------------------------------- /test/pgpool_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% ========================================================================================================== 2 | %% PGPool - A PosgreSQL client that automatically uses connection pools and reconnects in case of errors. 3 | %% 4 | %% The MIT License (MIT) 5 | %% 6 | %% Copyright (c) 2016-2019 Roberto Ostinelli and Neato Robotics, Inc. 7 | %% 8 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 9 | %% of this software and associated documentation files (the "Software"), to deal 10 | %% in the Software without restriction, including without limitation the rights 11 | %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | %% copies of the Software, and to permit persons to whom the Software is 13 | %% furnished to do so, subject to the following conditions: 14 | %% 15 | %% The above copyright notice and this permission notice shall be included in 16 | %% all copies or substantial portions of the Software. 17 | %% 18 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | %% THE SOFTWARE. 25 | %% ========================================================================================================== 26 | -module(pgpool_SUITE). 27 | 28 | %% callbacks 29 | -export([all/0]). 30 | -export([init_per_suite/1, end_per_suite/1]). 31 | -export([groups/0, init_per_group/2, end_per_group/2]). 32 | -export([init_per_testcase/2, end_per_testcase/2]). 33 | 34 | %% tests 35 | -export([ 36 | squery/1, 37 | equery/1, 38 | batch/1 39 | ]). 40 | -export([ 41 | squery_no_wait/1, 42 | equery_no_wait/1, 43 | batch_no_wait/1 44 | ]). 45 | 46 | %% include 47 | -include_lib("common_test/include/ct.hrl"). 48 | 49 | 50 | %% =================================================================== 51 | %% Callbacks 52 | %% =================================================================== 53 | 54 | %% ------------------------------------------------------------------- 55 | %% Function: all() -> GroupsAndTestCases | {skip,Reason} 56 | %% GroupsAndTestCases = [{group,GroupName} | TestCase] 57 | %% GroupName = atom() 58 | %% TestCase = atom() 59 | %% Reason = term() 60 | %% ------------------------------------------------------------------- 61 | all() -> 62 | [ 63 | {group, wait}, 64 | {group, no_wait} 65 | ]. 66 | 67 | %% ------------------------------------------------------------------- 68 | %% Function: groups() -> [Group] 69 | %% Group = {GroupName,Properties,GroupsAndTestCases} 70 | %% GroupName = atom() 71 | %% Properties = [parallel | sequence | Shuffle | {RepeatType,N}] 72 | %% GroupsAndTestCases = [Group | {group,GroupName} | TestCase] 73 | %% TestCase = atom() 74 | %% Shuffle = shuffle | {shuffle,{integer(),integer(),integer()}} 75 | %% RepeatType = repeat | repeat_until_all_ok | repeat_until_all_fail | 76 | %% repeat_until_any_ok | repeat_until_any_fail 77 | %% N = integer() | forever 78 | %% ------------------------------------------------------------------- 79 | groups() -> 80 | [ 81 | {wait, [shuffle], [ 82 | squery, 83 | equery, 84 | batch 85 | ]}, 86 | {no_wait, [shuffle], [ 87 | squery_no_wait, 88 | equery_no_wait, 89 | batch_no_wait 90 | ]} 91 | ]. 92 | %% ------------------------------------------------------------------- 93 | %% Function: init_per_suite(Config0) -> 94 | %% Config1 | {skip,Reason} | 95 | %% {skip_and_save,Reason,Config1} 96 | %% Config0 = Config1 = [tuple()] 97 | %% Reason = term() 98 | %% ------------------------------------------------------------------- 99 | init_per_suite(Config) -> 100 | %% set environments 101 | pgpool_test_suite_helper:set_environment_variables(), 102 | %% start pgpool 103 | pgpool:start(), 104 | %% return 105 | Config. 106 | 107 | %% ------------------------------------------------------------------- 108 | %% Function: end_per_suite(Config0) -> void() | {save_config,Config1} 109 | %% Config0 = Config1 = [tuple()] 110 | %% ------------------------------------------------------------------- 111 | end_per_suite(_Config) -> 112 | pgpool:stop(). 113 | 114 | %% ------------------------------------------------------------------- 115 | %% Function: init_per_group(GroupName, Config0) -> 116 | %% Config1 | {skip,Reason} | 117 | %% {skip_and_save,Reason,Config1} 118 | %% GroupName = atom() 119 | %% Config0 = Config1 = [tuple()] 120 | %% Reason = term() 121 | %% ------------------------------------------------------------------- 122 | init_per_group(_GroupName, Config) -> 123 | Config. 124 | 125 | %% ------------------------------------------------------------------- 126 | %% Function: end_per_group(GroupName, Config0) -> 127 | %% void() | {save_config,Config1} 128 | %% GroupName = atom() 129 | %% Config0 = Config1 = [tuple()] 130 | %% ------------------------------------------------------------------- 131 | end_per_group(_GroupName, _Config) -> 132 | ok. 133 | 134 | % ---------------------------------------------------------------------------------------------------------- 135 | % Function: init_per_testcase(TestCase, Config0) -> 136 | % Config1 | {skip,Reason} | {skip_and_save,Reason,Config1} 137 | % TestCase = atom() 138 | % Config0 = Config1 = [tuple()] 139 | % Reason = term() 140 | % ---------------------------------------------------------------------------------------------------------- 141 | init_per_testcase(_TestCase, Config) -> 142 | %% create table films 143 | {ok, [], []} = pgpool:squery(pgpool_test, "DROP TABLE IF EXISTS films;"), 144 | {ok, [], []} = pgpool:squery(pgpool_test, "CREATE TABLE films( 145 | id SERIAL PRIMARY KEY, 146 | name TEXT NOT NULL, 147 | year INT NOT NULL 148 | );"), 149 | %% return 150 | Config. 151 | 152 | % ---------------------------------------------------------------------------------------------------------- 153 | % Function: end_per_testcase(TestCase, Config0) -> 154 | % void() | {save_config,Config1} | {fail,Reason} 155 | % TestCase = atom() 156 | % Config0 = Config1 = [tuple()] 157 | % Reason = term() 158 | % ---------------------------------------------------------------------------------------------------------- 159 | end_per_testcase(_TestCase, _Config) -> 160 | %% drop table films 161 | {ok, [], []} = pgpool:squery(pgpool_test, "DROP TABLE IF EXISTS films;"). 162 | 163 | %% =================================================================== 164 | %% Tests 165 | %% =================================================================== 166 | squery(_Config) -> 167 | {ok, 1} = pgpool:squery(pgpool_test, "INSERT INTO films (name, year) VALUES ('First Movie', 1972);"), 168 | {ok, [ 169 | {column, <<"id">>, int4, _, _, _}, 170 | {column, <<"name">>, text, _, _, _}, 171 | {column, <<"year">>, int4, _, _, _} 172 | ], [ 173 | {<<"1">>, <<"First Movie">>, <<"1972">>} 174 | ]} = pgpool:squery(pgpool_test, "SELECT * FROM films WHERE year = 1972;"). 175 | 176 | equery(_Config) -> 177 | {ok, 1} = pgpool:equery(pgpool_test, "INSERT INTO films (name, year) VALUES ($1, $2);", ["First Movie", 1972]), 178 | {ok, [ 179 | {column, <<"id">>, int4, _, _, _}, 180 | {column, <<"name">>, text, _, _, _}, 181 | {column, <<"year">>, int4, _, _, _} 182 | ], [ 183 | {1, <<"First Movie">>, 1972} 184 | ]} = pgpool:equery(pgpool_test, "SELECT * FROM films WHERE year = $1;", [1972]). 185 | 186 | batch(_Config) -> 187 | S1 = "INSERT INTO films (name, year) VALUES ($1, $2);", 188 | [{ok, 1}, {ok, 1}] = pgpool:batch(pgpool_test, [ 189 | {S1, ["First Movie", 1972]}, 190 | {S1, ["Second Movie", 1978]} 191 | ]), 192 | 193 | S2 = "SELECT * FROM films WHERE year = $1;", 194 | [ 195 | {ok, [{1, <<"First Movie">>, 1972}]}, 196 | {ok, [{2, <<"Second Movie">>, 1978}]} 197 | ] = pgpool:batch(pgpool_test, [ 198 | {S2, [1972]}, 199 | {S2, [1978]} 200 | ]). 201 | 202 | squery_no_wait(_Config) -> 203 | %% block only available worker 204 | spawn(fun() -> pgpool:squery(pgpool_test, "select pg_sleep(1);") end), 205 | timer:sleep(200), 206 | %% return immediately 207 | {error, no_available_connections} = pgpool:squery( 208 | pgpool_test, 209 | "INSERT INTO films (name, year) VALUES ('First Movie', 1972);", 210 | [no_wait] 211 | ), 212 | %% wait for connection to be available 213 | timer:sleep(1000), 214 | {ok, 1} = pgpool:squery(pgpool_test, "INSERT INTO films (name, year) VALUES ('First Movie', 1972);"). 215 | 216 | equery_no_wait(_Config) -> 217 | %% block only available worker 218 | spawn(fun() -> pgpool:squery(pgpool_test, "select pg_sleep(1);") end), 219 | timer:sleep(200), 220 | %% return immediately 221 | {error, no_available_connections} = pgpool:equery( 222 | pgpool_test, 223 | "INSERT INTO films (name, year) VALUES ($1, $2);", ["First Movie", 1972], 224 | [no_wait] 225 | ), 226 | %% wait for connection to be available 227 | timer:sleep(1000), 228 | {ok, 1} = pgpool:equery(pgpool_test, "INSERT INTO films (name, year) VALUES ($1, $2);", ["First Movie", 1972]). 229 | 230 | batch_no_wait(_Config) -> 231 | %% block only available worker 232 | spawn(fun() -> pgpool:squery(pgpool_test, "select pg_sleep(1);") end), 233 | timer:sleep(200), 234 | %% return immediately 235 | S1 = "INSERT INTO films (name, year) VALUES ($1, $2);", 236 | {error, no_available_connections} = pgpool:batch( 237 | pgpool_test, [ 238 | {S1, ["First Movie", 1972]}, 239 | {S1, ["Second Movie", 1978]} 240 | ], 241 | [no_wait] 242 | ), 243 | %% wait for connection to be available 244 | timer:sleep(1000), 245 | [{ok, 1}, {ok, 1}] = pgpool:batch(pgpool_test, [ 246 | {S1, ["First Movie", 1972]}, 247 | {S1, ["Second Movie", 1978]} 248 | ]). 249 | -------------------------------------------------------------------------------- /test/pgpool_test_suite_helper.erl: -------------------------------------------------------------------------------- 1 | %% ========================================================================================================== 2 | %% PGPool - A PosgreSQL client that automatically uses connection pools and reconnects in case of errors. 3 | %% 4 | %% The MIT License (MIT) 5 | %% 6 | %% Copyright (c) 2016-2019 Roberto Ostinelli and Neato Robotics, Inc. 7 | %% 8 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 9 | %% of this software and associated documentation files (the "Software"), to deal 10 | %% in the Software without restriction, including without limitation the rights 11 | %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | %% copies of the Software, and to permit persons to whom the Software is 13 | %% furnished to do so, subject to the following conditions: 14 | %% 15 | %% The above copyright notice and this permission notice shall be included in 16 | %% all copies or substantial portions of the Software. 17 | %% 18 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | %% THE SOFTWARE. 25 | %% ========================================================================================================== 26 | -module(pgpool_test_suite_helper). 27 | 28 | %% API 29 | -export([set_environment_variables/0]). 30 | 31 | %% macros 32 | -define(PGPOOL_TEST_CONFIG_FILENAME, "pgpool-test.config"). 33 | 34 | %% =================================================================== 35 | %% API 36 | %% =================================================================== 37 | set_environment_variables() -> 38 | % read config file 39 | ConfigFilePath = filename:join([filename:dirname(code:which(?MODULE)), ?PGPOOL_TEST_CONFIG_FILENAME]), 40 | {ok, [AppsConfig]} = file:consult(ConfigFilePath), 41 | % loop to set variables 42 | F = fun({AppName, AppConfig}) -> 43 | set_environment_for_app(AppName, AppConfig) 44 | end, 45 | lists:foreach(F, AppsConfig). 46 | 47 | %% =================================================================== 48 | %% Internal 49 | %% =================================================================== 50 | set_environment_for_app(AppName, AppConfig) -> 51 | F = fun({Key, Val}) -> 52 | ok = application:set_env(AppName, Key, Val) 53 | end, 54 | lists:foreach(F, AppConfig). 55 | -------------------------------------------------------------------------------- /test/results/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ostinelli/pgpool/f733a321033ba85da12d4c27e408e6b7fb7b30cc/test/results/.keep --------------------------------------------------------------------------------