Skip to content

Commit 753b760

Browse files
committed
roadrunner: replace the hand-rolled DB pool with ecpool
1 parent 79ae160 commit 753b760

4 files changed

Lines changed: 53 additions & 29 deletions

File tree

frameworks/roadrunner/rebar.config

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44

55
{deps, [
66
{roadrunner, "0.8.0"},
7-
{epgsql, "4.8.0"}
7+
{epgsql, "4.8.0"},
8+
{ecpool, "0.4.2"}
89
]}.
910

1011
{relx, [
1112
{release, {roadrunner_httparena, "0.1.0"},
12-
[roadrunner_httparena, roadrunner]},
13+
[roadrunner_httparena, ecpool, roadrunner]},
1314
{sys_config, "./config/sys.config"},
1415
{dev_mode, false},
1516
{include_erts, true},

frameworks/roadrunner/rebar.lock

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
{"1.2.0",
2-
[{<<"epgsql">>,{pkg,<<"epgsql">>,<<"4.8.0">>},0},
2+
[{<<"ecpool">>,{pkg,<<"ecpool">>,<<"0.4.2">>},0},
3+
{<<"epgsql">>,{pkg,<<"epgsql">>,<<"4.8.0">>},0},
4+
{<<"gproc">>,{pkg,<<"gproc">>,<<"0.8.0">>},1},
35
{<<"roadrunner">>,{pkg,<<"roadrunner">>,<<"0.8.0">>},0},
46
{<<"telemetry">>,{pkg,<<"telemetry">>,<<"1.4.2">>},1}]}.
57
[
68
{pkg_hash,[
9+
{<<"ecpool">>, <<"2F466096B81B9C945494CDF6A554ACAD4C31B0AC5A3D2B1929B822BA7C9B9F6E">>},
710
{<<"epgsql">>, <<"C491B141B8C37BCE7B67F2079F168F339BB374A7CF9A286CB3B40AD1CD3FABE5">>},
11+
{<<"gproc">>, <<"CEA02C578589C61E5341FCE149EA36CCEF236CC2ECAC8691FBA408E7EA77EC2F">>},
812
{<<"roadrunner">>, <<"7BBC9DA8E2085EA94FE0A06DB51DBAD762AA016E70972F2A533C3914438C96F6">>},
913
{<<"telemetry">>, <<"A0CB522801DFFB1C49FE6E30561BADFFC7B6D0E180DB1300DF759FAA22062855">>}]},
1014
{pkg_hash_ext,[
15+
{<<"ecpool">>, <<"4E834C138ABF742F281ECEBEF69D37C6D7BF0A4E8E23965CF3826E027B723ED2">>},
1116
{<<"epgsql">>, <<"00E550006A62FB439FC7E879419443C889C34605E9B9DC406029D8BAA1AD79D8">>},
17+
{<<"gproc">>, <<"580ADAFA56463B75263EF5A5DF4C86AF321F68694E7786CB057FD805D1E2A7DE">>},
1218
{<<"roadrunner">>, <<"76F3389A27CE084FF380F80C6DE73C2448A6702017A2C22ACF34DE606C8239D1">>},
1319
{<<"telemetry">>, <<"928F6495066506077862C0D1646609EED891A4326BEE3126BA54B60AF61FEBB1">>}]}
1420
].

frameworks/roadrunner/src/roadrunner_httparena.app.src

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
sasl,
1010
ssl,
1111
epgsql,
12+
ecpool,
1213
roadrunner
1314
]},
1415
{env, []},

frameworks/roadrunner/src/roadrunner_httparena_db.erl

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
-module(roadrunner_httparena_db).
22

3-
-export([start_pool/0, query/2]).
3+
-behaviour(ecpool_worker).
44

5-
%% Connections live in a persistent_term tuple, addressed lock-free per
6-
%% request (no central checkout broker, so nothing serializes the pool).
7-
-define(CONNS_KEY, {?MODULE, conns}).
5+
-export([start_pool/0, connect/1, query/2]).
6+
7+
%% `connect/1` is the ecpool_worker callback, invoked by ecpool via the
8+
%% `Mod:connect/1` MFA when it opens (or reopens) a pooled connection, not
9+
%% by a static call, so xref cannot see the reference.
10+
-ignore_xref([{connect, 1}]).
11+
12+
-define(POOL, httparena_pg).
813

914
%% Every SQL the adapter runs, keyed by an atom name. Each statement is
10-
%% parsed once per connection at connect time (named after its key), so the
11-
%% hot path runs `epgsql:prepared_query/3` against a cached `#statement{}`
12-
%% record instead of re-parsing on every request.
15+
%% parsed once per pooled connection at connect time (named after its key),
16+
%% so the hot path runs `epgsql:prepared_query/3` against a cached
17+
%% `#statement{}` record instead of re-parsing on every request.
1318
statements() ->
1419
#{
1520
read => ~"""
@@ -51,32 +56,42 @@ statements() ->
5156
fortunes => ~"SELECT id, message FROM fortune"
5257
}.
5358

59+
%% Start an ecpool pool of `pool_size()` connections. ecpool routes each
60+
%% request to a worker via `gproc_pool` (an ETS lookup, no central checkout
61+
%% broker), so concurrent callers spread across connections instead of
62+
%% serializing through one coordinator. `auto_reconnect` makes a worker
63+
%% reopen (and re-prepare) its connection if the backend drops it.
5464
-spec start_pool() -> ok | disabled.
5565
start_pool() ->
5666
case os:getenv("DATABASE_URL") of
5767
false ->
5868
disabled;
5969
Url ->
6070
ConnMap = parse_url(Url),
61-
Size = pool_size(),
62-
Conns = list_to_tuple([connect(ConnMap) || _ <- lists:seq(1, Size)]),
63-
persistent_term:put(?CONNS_KEY, Conns),
71+
Opts = [
72+
{pool_size, pool_size()},
73+
{pool_type, random},
74+
{auto_reconnect, 2},
75+
{conn_map, ConnMap}
76+
],
77+
{ok, _} = ecpool:start_sup_pool(?POOL, ?MODULE, Opts),
6478
ok
6579
end.
6680

67-
%% Open a connection and parse every statement on it (the prepared
68-
%% statement must exist on each backend connection). The resulting
69-
%% `#statement{}` metadata is connection-independent, so we cache one per
70-
%% name in `persistent_term`; concurrent connects racing to put the same
71-
%% value is harmless.
72-
-spec connect(epgsql:connect_opts()) -> epgsql:connection().
73-
connect(ConnMap) ->
81+
%% ecpool_worker callback: open a connection and parse every statement on it
82+
%% (the prepared statement must exist on each backend connection). The
83+
%% resulting `#statement{}` metadata is connection-independent, so we cache
84+
%% one per name in `persistent_term`; connections racing to put the same
85+
%% value (including after a reconnect) is harmless.
86+
-spec connect([term()]) -> {ok, epgsql:connection()}.
87+
connect(Opts) ->
88+
{conn_map, ConnMap} = lists:keyfind(conn_map, 1, Opts),
7489
{ok, Conn} = epgsql:connect(ConnMap),
7590
ok = maps:foreach(
7691
fun(Name, Sql) -> prepare(Conn, Name, Sql) end,
7792
statements()
7893
),
79-
Conn.
94+
{ok, Conn}.
8095

8196
prepare(Conn, Name, Sql) ->
8297
{ok, Stmt} = epgsql:parse(Conn, atom_to_list(Name), Sql, []),
@@ -86,17 +101,18 @@ prepare(Conn, Name, Sql) ->
86101
_ -> ok
87102
end.
88103

89-
%% Pick a connection at random and run the prepared statement on it
90-
%% directly. epgsql serializes commands per connection (its gen_server
91-
%% mailbox), so concurrent callers queue on the chosen connection instead
92-
%% of funneling through one checkout broker — no broker bottleneck and no
93-
%% `no_members` refusals under high concurrency.
104+
%% Run the prepared statement on a pool-picked connection. ecpool hands the
105+
%% callback the connection chosen for this request; epgsql serializes
106+
%% commands per connection through its own mailbox, so callers queue on the
107+
%% chosen connection, never on a shared broker. A worker mid-reconnect
108+
%% yields `{error, disconnected}`, which the handlers map to a 5xx.
94109
-spec query(atom(), [term()]) ->
95110
{ok, list(), list()} | {ok, non_neg_integer()} | {error, term()}.
96111
query(Name, Params) ->
97-
Conns = persistent_term:get(?CONNS_KEY),
98-
Conn = element(rand:uniform(tuple_size(Conns)), Conns),
99-
epgsql:prepared_query(Conn, persistent_term:get({?MODULE, stmt, Name}), Params).
112+
Stmt = persistent_term:get({?MODULE, stmt, Name}),
113+
ecpool:with_client(?POOL, fun(Conn) ->
114+
epgsql:prepared_query(Conn, Stmt, Params)
115+
end).
100116

101117
parse_url(Url) ->
102118
Parsed = uri_string:parse(Url),

0 commit comments

Comments
 (0)