Module eysql_pool

A fixed-size pool of epgsql connections spread across a cluster.

Behaviours: gen_server.

Description

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:

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.

The cluster marks no host for it: a failing hook is the application's, not the server's, and leaving servers out for it would let one bad statement take every server out of rotation. The pool steers its own connections instead, but only between the hosts a pick would choose among anyway (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.

Data Types

watch()

abstract datatype: watch()

Function Index

checkin/2Return a connection for reuse.
checkout/2Take a connection, waiting up to Timeout ms for one.
cluster/1
discard/2Return a connection that must not be reused, for example after an exception left a transaction open.
driver/1The 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/2Stop the clock that watch/1 started.
watch/1Start the clock on a call to the server on Conn, for a driver to call just before the call.

Function Details

checkin/2

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/2

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/1

cluster(Pool::gen_server:server_ref()) -> pid()

discard/2

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/1

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/1

format_status(Status) -> any()

handle_call/3

handle_call(Request, From, State) -> any()

handle_cast/2

handle_cast(Message, State) -> any()

handle_info/2

handle_info(Message, State) -> any()

init/1

init(Config) -> any()

start_link/1

start_link(Config::eysql_config:config()) -> {ok, pid()} | {error, term()}

start_link/2

start_link(Name::gen_server:server_name(), Config::eysql_config:config()) -> {ok, pid()} | {error, term()}

stats/1

stats(Pool::gen_server:server_ref()) -> #{atom() => term()}

stop/1

stop(Pool::gen_server:server_ref()) -> ok

terminate/2

terminate(Reason, State) -> any()

unwatch/2

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/1

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.

The clock is a timer to the pool, which kills the connection if the call is still waiting when it runs out: the socket closes, and the call fails as on any connection that dies. That sends nothing to the server and costs a timer, started and cancelled, on each call. The pool has to be the one to act, because the calling process is blocked in the call. eysql_conn runs every query it makes on a connection this way.


Generated by EDoc