Topology-aware load balancing and failover for epgsql.
eysql gives epgsql what the YugabyteDB smart drivers give JDBC, pgx and
psycopg: it discovers the cluster's servers with yb_servers(), opens each
connection to the least-loaded server in the preferred zone or region, and
skips hosts that fail. It adds a pool that recycles and rebalances
connections, and a transaction helper that retries serialization
failures. With PostgreSQL it uses the configured hosts.
Load balancing is off unless load_balance says otherwise, as in the
smart drivers: without it, the pool uses the configured hosts only.
Connections are ordinary epgsql connections: check one out and call epgsql on it.
{ok, Pool} = eysql:start_link(#{hosts => ["yb-tservers.db.svc.cluster.local"],
username => <<"app">>,
password => <<"secret">>,
database => <<"app">>,
load_balance => true,
topology_keys => "gcp.us-east1.us-east1-b:1,gcp.us-east1.*:2"}),
{ok, _, Rows} = eysql:equery(Pool, "SELECT id FROM accounts WHERE realm = $1", [Realm]),
eysql:with_connection(Pool, fun(Conn) -> epgsql:equery(Conn, Sql, Params) end).
pool() = gen_server:server_ref()
transaction_error() = {rollback, term()} | commit_outcome_unknown | transaction_aborted | checkout_timeout | term()
| checkin/2 | Return a connection for reuse. |
| checkout/1 | |
| checkout/2 | Take an epgsql connection, waiting up to Timeout ms. |
| child_spec/2 | A child spec for a registered pool, Name being a local name. |
| cluster_info/1 | The cluster's view: servers, where new connections go now
(placement), which hosts the pool keeps connections on (allowed),
counts per host, hosts that failed and have not accepted a connection
since (failed), and standbys that target_session_attrs => read_write
turned away (read_only). |
| discard/2 | Return a connection that must not be reused, for example after an error left it in an unknown state. |
| equery/3 | epgsql:equery/3 on a pooled connection, through
eysql_conn:equery/3: bounded by socket_timeout, and a lost
connection is an error rather than an exit. |
| squery/2 | epgsql:squery/2 on a pooled connection, as equery/3. |
| start_link/1 | Start a pool linked to the caller. |
| start_link/2 | Start a registered pool, for example {local, my_db}. |
| stats/1 | Connections by state and by host. |
| stop/1 | |
| transaction/2 | Equivalent to transaction(Pool, Fun, #{}).
|
| transaction/3 | Run Fun inside BEGIN and COMMIT, retrying when that is safe. |
| with_connection/2 | Equivalent to with_connection(Pool, Fun, 5000).
|
| with_connection/3 | Run Fun with a connection and return its result. |
checkin(Pool::pool(), Conn::pid()) -> ok
Return a connection for reuse. Only the process that checked it out can return it: the pool ignores a checkin from any other process, including a late one from an earlier holder. If the holder exits instead, the pool closes the connection and opens another.
The pool does not look at the connection: the next holder gets it as it is. End any transaction with COMMIT or ROLLBACK first, or calldiscard/2 instead. eysql_conn:transaction_status/1 tells
whether one is still open; with_connection/3 does this for you.
checkout(Pool::pool()) -> {ok, pid()} | {error, term()}
checkout(Pool::pool(), Timeout::timeout()) -> {ok, pid()} | {error, term()}
Take an epgsql connection, waiting up to Timeout ms. Return it with
checkin/2, or discard/2 if it may be in a bad state.
Until then, the pool's socket_timeout bounds the queries this process
makes on it with eysql_conn:squery/2 and
eysql_conn:equery/3.
child_spec(Name::atom(), Options::eysql_config:options()) -> supervisor:child_spec()
A child spec for a registered pool, Name being a local name.
cluster_info(Pool::pool()) -> #{atom() => term()}
The cluster's view: servers, where new connections go now
(placement), which hosts the pool keeps connections on (allowed),
counts per host, hosts that failed and have not accepted a connection
since (failed), and standbys that target_session_attrs => read_write
turned away (read_only). See eysql_cluster:snapshot/1.
discard(Pool::pool(), Conn::pid()) -> ok
Return a connection that must not be reused, for example after an
error left it in an unknown state. The pool closes it and opens another.
As with checkin/2, only the process holding the connection can
discard it; a discard from any other process is ignored.
equery(Pool::pool(), Sql::iodata(), Params::[term()]) -> term()
epgsql:equery/3 on a pooled connection, through
eysql_conn:equery/3: bounded by socket_timeout, and a lost
connection is an error rather than an exit.
squery(Pool::pool(), Sql::iodata()) -> term()
epgsql:squery/2 on a pooled connection, as equery/3.
start_link(Options::eysql_config:options()) -> {ok, pid()} | {error, term()}
Start a pool linked to the caller. See eysql_config:defaults/0
for the options.
start_link(Name::gen_server:server_name(), Options::eysql_config:options()) -> {ok, pid()} | {error, term()}
Start a registered pool, for example {local, my_db}.
stats(Pool::pool()) -> #{atom() => term()}
Connections by state and by host.
stop(Pool::pool()) -> ok
transaction(Pool::pool(), Fun::fun((pid()) -> {ok, T} | {rollback, term()} | {error, term()})) -> {ok, T} | {error, transaction_error()}
Equivalent to transaction(Pool, Fun, #{}).
transaction(Pool::pool(), Fun::fun((pid()) -> {ok, T} | {rollback, term()} | {error, term()}), Options::map()) -> {ok, T} | {error, transaction_error()}
Run Fun inside BEGIN and COMMIT, retrying when that is safe.
Fun returns {ok, Value} to commit, {rollback, Reason} to roll back,
or {error, Reason}, usually an epgsql error it got back, to roll back.
The whole transaction runs again, on a fresh checkout, when an error is
retryable (serialization failure or deadlock, see eysql_error) or
the connection was lost before COMMIT. Nothing was committed in either case.
Fun must therefore have no side effects outside the database.
The pool's socket_timeout bounds BEGIN, COMMIT and ROLLBACK, and the
queries Fun makes with eysql_conn:squery/2 and
eysql_conn:equery/3, as in with_connection/3. One that
runs out loses the connection: before COMMIT, the transaction runs again;
during COMMIT, the result is commit_outcome_unknown.
{ok, Value}: committed;{error, {rollback, Reason}}: Fun asked to roll back;{error, commit_outcome_unknown}: the connection was lost during
COMMIT, so the transaction may or may not have committed. Check the
data, for example by re-reading a row the transaction wrote;{error, transaction_aborted}: COMMIT rolled back because a statement
failed earlier and Fun still returned {ok, _};{error, Reason}: anything else, including the last error once
retries run out.If no connection can be checked out for a retry, because none came in time
or the pool has stopped, the retries stop there and the result is the
error that made the last attempt retry, such as the serialization failure,
as when the attempts run out. A checkout error such as
{error, checkout_timeout} therefore means Fun never ran.
attempts: how many times to run the transaction, default 3;checkout_timeout: ms to wait for a connection, default 5000;max_backoff: the longest wait before a retry, in ms, default 500.
The wait is random: up to 20 ms before the second attempt, twice that
before the third and so on, but never more than this. 0 retries at
once;begin: the statement that opens the transaction, default
"BEGIN";driver: the module that runs BEGIN, COMMIT and ROLLBACK, by default
the pool's own driver option. Only tests need to set it.Fun raises, the connection is discarded, which ends the transaction,
and the exception is re-raised.
with_connection(Pool::pool(), Fun::fun((pid()) -> Result)) -> Result | {error, term()}
Equivalent to with_connection(Pool, Fun, 5000).
with_connection(Pool::pool(), Fun::fun((pid()) -> Result), Timeout::timeout()) -> Result | {error, term()}
Run Fun with a connection and return its result. Returns
{error, Reason} if no connection could be had.
The connection goes back to the pool afterwards only if no transaction is
open on it. If Fun ran BEGIN without COMMIT or ROLLBACK, or left the
connection in a state the driver's transaction_status/1 cannot read
(see eysql_conn:transaction_status/1), the connection is discarded
instead: the pool closes it, which rolls the transaction back, and opens
another. The check costs no round trip to the server. If Fun raises, the
connection is discarded and the exception re-raised.
With the pool's socket_timeout set, queries Fun makes with
eysql_conn:squery/2 and eysql_conn:equery/3 wait for the
server that long at most, then return
{error, {connection_lost, socket_timeout}}, the connection closed.
epgsql's own functions, called on the connection directly, wait for as
long as the server takes.
Fun runs, the result of Fun is still
returned. The pool closes its connections as it stops, so that is usually
an error that eysql_error:is_connection_lost/1 accepts, such as
{error, closed}.
Generated by EDoc