Behaviours: gen_server.
A fixed-size pool of epgsql connections spread across a cluster.
The pool starts its own eysql_cluster and keeps pool_size
connections open, each opened in a short-lived process so a slow connect
never blocks checkouts. It starts opening them at once: each opener waits
in eysql_cluster:open/1 for the cluster's first discovery, so the
first connections go to the discovered servers rather than all to the
seeds, and checkouts wait for the openers. On top of what a smart driver
does, it:
max_lifetime plus up to
lifetime_jitter, so connections return to hosts that came back. An
idle connection past its lifetime closes at the next rebalance tick,
or when a checkout comes to it first; a leased one, when it comes
back;rebalance_batch connections every rebalance_interval
off hosts no longer allowed, or else from the busiest host new
connections may go to, and reopens them where the cluster says. A
host that failed stays allowed, and connections move back towards a
better one only once a connection to it has opened again, the
cluster's probe at a refresh as a rule. Until then it does not count
towards finding the busiest either. With load_balance false new
connections go to the first host that works, so there is no busiest
to move from, and connections on a later host move back to the first
once it is found back. A rebalance asks the cluster process only; it sends nothing to
a server.Like the smart drivers, the pool sends no query of its own: no health
check, no ping. A connection that dies is found as its process exits,
which epgsql's does when the server closes the socket or the socket
fails; and a connection that fails a query with a lost connection is
discarded by eysql:with_connection/3 and
eysql:transaction/3, which read its transaction status as
unknown. Either way the pool opens another, and only if that connect
fails is the host marked down. A query error on a live connection marks
nothing: a cancelled or slow statement never takes a host out.
A server that dies without closing its sockets, or hangs, answers nothing
and closes nothing, so a query on it would wait for as long as TCP takes
to give up. socket_timeout bounds that wait on the client, over the
whole call where pgjdbc's socketTimeout bounds each read, and sends
nothing to the server to do it. A checkout hands the bound to the process
that checks the connection out, and eysql_conn:squery/2 and
eysql_conn:equery/3 honour it there (see watch/1). That
covers eysql:equery/3, eysql:squery/2, the BEGIN, COMMIT
and ROLLBACK of eysql:transaction/3, and those two functions
called inside eysql:with_connection/3 or a transaction. A call
that overruns has its connection killed, which closes the socket, and
returns {error, {connection_lost, socket_timeout}}. The pool replaces
the connection as it replaces any that dies, and marks no host: a
timeout on an open connection is not a failure to connect. epgsql calls
made on the connection directly are not bounded.
Connections are linked to the pool, which traps exits: a connection that dies is replaced, and if the pool stops its connections and its cluster go with it. A process that dies while holding a connection loses it; the pool closes that connection, since its state is unknown, and opens another. Only the process holding a connection can return it.
after_connect prepares each connection the pool opens before anyone
gets it, replacements included: those for a connection that died, one
past its lifetime, or one moved by a rebalance. The opener calls it with
the connection once eysql_cluster:open/1 has opened it, in a
process of its own, and reports the connection only when it is done.
Until then the connection counts as opening, and checkouts wait for
it. ok, or a tuple whose first element is ok, lets it into the pool.
Anything else fails it: {error, Reason}, an exception, another value,
no answer within after_connect_timeout, or a connection the hook left
closed or inside a transaction. The opener closes the connection and
reports the failure.
eysql_cluster:pick/3): a hook failure never changes
the tier, topology level, node type or host order. For the cluster's
failed-host delay after a hook fails on a host, new connections go to the
other hosts the pick offers, if it offers any, and rebalancing moves
nothing towards the host. A failure on a host the pool was not keeping
off is replaced at once, which then keeps off it. A failure on one it
had to take, the pick offering nothing else, as with load_balance
false or a single host in the preferred level, is a failed open: the
pool opens another after its refill delay, and fails waiting checkouts
with {error, {after_connect, Reason}} when no other connection is
coming. After the delay, picks and rebalancing try the host again. The
first failure on a host is logged as a warning with the reason, and the
end of the run once, at info, as the cluster logs a host's failed
connects. The connections that discovery and probes open are the
cluster's, and never run the hook.
abstract datatype: watch()
| checkin/2 | Return a connection for reuse. |
| checkout/2 | Take a connection, waiting up to Timeout ms for one. |
| cluster/1 | |
| discard/2 | Return a connection that must not be reused, for example after an exception left a transaction open. |
| driver/1 | The driver the pool opens its connections with, the driver option. |
| format_status/1 | |
| handle_call/3 | |
| handle_cast/2 | |
| handle_info/2 | |
| init/1 | |
| start_link/1 | |
| start_link/2 | |
| stats/1 | |
| stop/1 | |
| terminate/2 | |
| unwatch/2 | Stop the clock that watch/1 started. |
| watch/1 | Start the clock on a call to the server on Conn, for a driver to
call just before the call. |
checkin(Pool::gen_server:server_ref(), Conn::pid()) -> ok
Return a connection for reuse. Call it from the process that checked the connection out; the pool ignores a checkin from any other process.
checkout(Pool::gen_server:server_ref(), Timeout::timeout()) -> {ok, pid()} | {error, term()}
Take a connection, waiting up to Timeout ms for one. With a
socket_timeout, the calling process keeps it for the connection until
it checks the connection in or discards it (see watch/1).
cluster(Pool::gen_server:server_ref()) -> pid()
discard(Pool::gen_server:server_ref(), Conn::pid()) -> ok
Return a connection that must not be reused, for example after an
exception left a transaction open. The pool closes it and opens another.
As with checkin/2, only the process holding it can discard it.
driver(Pool::gen_server:server_ref()) -> module()
The driver the pool opens its connections with, the driver option.
It is fixed for the pool's life. A pool on this node keeps it in
persistent_term, so this makes no call to the pool; a pool on another
node is asked.
format_status(Status) -> any()
handle_call(Request, From, State) -> any()
handle_cast(Message, State) -> any()
handle_info(Message, State) -> any()
init(Config) -> any()
start_link(Config::eysql_config:config()) -> {ok, pid()} | {error, term()}
start_link(Name::gen_server:server_name(), Config::eysql_config:config()) -> {ok, pid()} | {error, term()}
stats(Pool::gen_server:server_ref()) -> #{atom() => term()}
stop(Pool::gen_server:server_ref()) -> ok
terminate(Reason, State) -> any()
unwatch(Timer::watch(), Conn::pid()) -> ok | expired
Stop the clock that watch/1 started. expired if it ran out
first: the pool is killing the connection, or has, even if the call got
its answer at the last moment. This kills it as well, so that it is dead
before anything else this process sends it, such as the status read
that decides whether it goes back to the pool. The caller should report
the call as {error, {connection_lost, socket_timeout}}.
watch(Conn::pid()) -> watch()
Start the clock on a call to the server on Conn, for a driver to
call just before the call. It runs only in the process that checked
Conn out of a pool with a socket_timeout, and until that process
checks it in or discards it; elsewhere this does nothing. Follow with
unwatch/2 as soon as the call returns or raises.
eysql_conn runs every query it makes on a connection this way.
Generated by EDoc