Module eysql_cluster

One process per database cluster: which servers exist, how many connections this node holds to each, and which hosts have failed.

Behaviours: gen_server.

Description

One process per database cluster: which servers exist, how many connections this node holds to each, and which hosts have failed.

It refreshes on a timer, early after any connection failure, and at start unless load_balance is false. A pick also starts a refresh once the refresh interval has passed, without waiting for it; with an interval of 0 every pick does, and there is no timer. A pick that finds no host starts none: as in the JDBC smart driver, only the interval, a connection failure or a failed discovery makes a refresh due. A refresh discovers servers with yb_servers(), and probes the hosts that have failed (see below). On PostgreSQL, where that function does not exist, and with load_balance false, it stays with the configured hosts, and a refresh only probes. connect/1 asks it for a host and opens the connection in the calling process, so a slow connect never blocks the cluster process.

A discovery asks the configured hosts, the seeds, first, each once and in the order given, and then the discovered servers that are not down, as the JDBC driver's checkAndRefresh dials the hosts in its URL before the servers its getAllAvailableHosts lists. It never dials a server that is down.

A discovery that fails leaves the refresh due, as the JDBC driver's failed refresh leaves its clock where it was: the next pick starts another, though not within a second of the last refresh, and the timer comes back after the full interval, not sooner. So while discovery fails, it is tried again at most once a second while connections are being opened, and once per interval while none are, and each of those refreshes probes the failed hosts that are due. The driver would retry on every connection, and un-mark its down hosts only in a refresh that succeeded; this keeps a busy pool from hammering a dead cluster, and still brings connections back within seconds of a full outage. A discovery that succeeds probes at once the failed hosts that are due and have no probe running, so that they come back with the cluster rather than at the next refresh. A run of failed discoveries is logged once, as a warning, and its end once, at info, by the rules for a host's failures below.

connect/1 waits for the first discovery to finish, either way, as the JDBC driver runs its first refresh before it picks a server. It then tries each host a pick offers, at most once each, until one opens: every eligible server, least loaded first, a tie going to one at random, and then the seeds, as the driver's getConnection tries every eligible server and its caller then makes a plain connection to the hosts in its URL.

The seeds are taken in the order given, as that plain connection takes the hosts in the URL (pgjdbc's loadBalanceHosts is false by default): a connection goes to the first seed that is working, and to the next only when that one fails it. With load_balance false the seeds are all there is, so every connection goes to the first host that works; with target_session_attrs => read_write, to the first that accepts writes.

Each server has two addresses, its host and its public IP, and which one connections go to is decided as the JDBC driver's refresh decides it (eysql_topology:address_column/3). The discovery process resolves the name it dialled and each server's two names, and compares addresses: the first server whose host is the address that answered, or whose public IP is, or whose two names are one address, decides for the life of the cluster process. A client outside the cluster's network reaches it at a public IP, and so connects to each server's. Until a discovery decides, each connects to the public IPs when every server has one and every one resolves, and to the hosts otherwise.

While no discovered server is working, a pick takes the first seed that is; and while no seed is working either, the seeds regardless, as a smart driver falls back to a plain connection to the hosts in its URL. It does neither in the modes where the JDBC driver refuses instead of falling back: only_primary, only_rr, and fallback_to_topology_keys_only with topology keys under true or any. There, once servers are discovered, a pick that finds no eligible server fails with {no_node_available, primary | read_replica | cluster}, as the driver's getLeastLoadedServer throws "No node available", whether or not the connect has tried servers already. Before a discovery has succeeded, and on PostgreSQL, the seeds are all there is, and every mode takes them, whatever their node type: the driver makes a plain connection to its URL hosts when its refresh fails, or finds no yb_servers().

Only a failure to connect marks a host down: the failures pgjdbc reports as SQLSTATE 08001, such as a refused or timed-out connect or a name that does not resolve (eysql_error:connect_failure/1). A host that answers but fails the connection otherwise, with a failed login, a missing database, too many connections, a server starting up or shutting down, or a failed TLS handshake, is not down; it is kept apart as rejected. Either way, and for a standby (below), the host is left out of picks until a refresh's probe, or a connection to it that opens, finds it back. The smart drivers do that for a host that is down: they mark it down, and un-mark it only at a refresh once its delay has passed. A rejected host they skip only for the rest of that one connection, and their next connection tries it again. Here it stays out, so that a server that turns connections away, as one does while it restarts, takes no new connection until a probe finds it taking them, and takes them again from that refresh on. Only the seeds' last resort above takes a failed host, and then only a seed, once per connection. Nothing that happens on a connection once it is open marks its host: not a query error, not a cancelled or slow statement, not one that socket_timeout cuts off, and not the connection's death. Only open_failed and the probes below mark hosts, and both follow a connect.

At each refresh the cluster probes every failed host, down, standby or rejected, whose delay has passed: it connects in a separate process, with the same target_session_attrs check as a pick's connect, and closes the connection. A probe that succeeds ends the failure, as does any connection to the host that opens. One that fails, or takes longer than twice connect_timeout, starts another delay, as a failure of the kind its reason makes it, and starts no refresh: a server that answers 57P03 while it shuts down, and then refuses connections, turns from rejected to down. So a host that stays failed is tried once per refresh, not once per delay. There is at most one probe per host, it is not counted as a connection, and it dies with the cluster. The cluster traps exits for that: probes are linked to it, and it kills them when it stops.

A standby that target_session_attrs => read_write turns away is reachable, but has the wrong role. It is left out and probed like a host that is down, so a promotion is found at the next refresh, or at once through the seeds when the primary fails. snapshot/1 reports it apart from the hosts that are down.

A host that fails is logged once, as a warning with the reason when the caller gives one, and not again for the rest of that run of failures. A standby is logged the same way, at info. The run ends once a connection to the host, a probe's included, has opened since its last failure and a minute has passed since that failure; that is logged at info. A failure of another kind starts a new run: down, standby and rejected are each logged in their own words, so that a failed login never reads as a host that is down.

A discovery that succeeds forgets the failures, runs and probes of hosts that are neither servers nor seeds any more.

Counts are per cluster process, like a smart driver's are per client: connections opened through connect/1 are monitored and counted until they close. A host picked for a connection counts as pending until the picking process reports how the connect went, or exits.

Data Types

cluster()

cluster() = pid()

connect_spec()

connect_spec() = #{driver := module(), settings := map(), target_session_attrs := any | read_write}

pick_error()

pick_error() = no_server_available | {no_node_available, primary | read_replica | cluster}

reservation()

reservation() = {reference(), eysql_topology:key()}

Function Index

cancel/2Give Reservation back with no outcome, for example because the connect raised.
connect/1Open a connection to the host this cluster picks.
format_status/1
handle_call/3
handle_cast/2
handle_info/2
init/1
open/1As connect/1, also returning the host the connection went to.
open/2As open/1, preferring the hosts a pick would choose among that are not in Avoid (see pick/3).
open_failed/2The connection for Reservation could not be made; picks leave its host out until a refresh finds it back, as after a reason that eysql_error:connect_failure/1 calls unreachable.
open_failed/3As open_failed/2, failed with Reason, which decides what the host counts as.
opened/3The connection for Reservation is open; count it until its process exits.
pick/1Reserve the best host for a new connection.
pick/2As pick/1, leaving out the hosts in Tried, those the caller has tried already for this connection.
pick/3As pick/2, choosing among the hosts it would choose among less those in Avoid, or among all of them when that leaves none.
refresh/1Refresh now: read yb_servers() again, unless on PostgreSQL, and probe the failed hosts whose delay has passed.
snapshot/1The cluster's current view.
start_link/1Start a cluster process from a normalized config (eysql_config).
stop/1
terminate/2
when_ready/1ready if the first discovery has finished (or there is none to do); otherwise pending, and the caller gets {eysql_cluster_ready, Cluster} when it finishes.

Function Details

cancel/2

cancel(Cluster::cluster(), X2::reservation()) -> ok

Give Reservation back with no outcome, for example because the connect raised. Its host is not left out.

connect/1

connect(Cluster::cluster()) -> {ok, pid()} | {error, term()}

Open a connection to the host this cluster picks. Runs in the caller; the connection is linked to the caller, as with epgsql.

Waits first for the cluster's first discovery to finish, either way, as the JDBC driver runs its first refresh before it picks; the discovery's own timeout bounds the wait. Then tries hosts until one opens, each at most once: every server a pick allows, least loaded first, then the seeds in the order given, where the mode allows them. That is the JDBC driver's getConnection loop, which tries every eligible server, followed by its plain connection to the hosts in its URL.

Returns the last connect error when every host tried failed, and {error, no_server_available} when no host was left to try. In the modes where the driver refuses rather than fall back to the seeds, once servers are discovered, it returns {error, {no_node_available, Type}} when no eligible server is left, whether or not it tried some first, as the driver's last getLeastLoadedServer throws.

format_status/1

format_status(Status) -> any()

handle_call/3

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

handle_cast/2

handle_cast(Message, State) -> any()

handle_info/2

handle_info(Message, State) -> any()

init/1

init(Config) -> any()

open/1

open(Cluster::cluster()) -> {ok, pid(), eysql_topology:key()} | {error, term()}

As connect/1, also returning the host the connection went to.

open/2

open(Cluster::cluster(), Avoid::[eysql_topology:key()]) -> {ok, pid(), eysql_topology:key()} | {error, term()}

As open/1, preferring the hosts a pick would choose among that are not in Avoid (see pick/3). The pool passes the hosts its after_connect hook has just failed on. It marks nothing: other callers still connect to those hosts.

open_failed/2

open_failed(Cluster::cluster(), Reservation::reservation()) -> ok

The connection for Reservation could not be made; picks leave its host out until a refresh finds it back, as after a reason that eysql_error:connect_failure/1 calls unreachable. As open_failed/3, with no reason to log.

open_failed/3

open_failed(Cluster::cluster(), Reservation::reservation(), Reason::term()) -> ok

As open_failed/2, failed with Reason, which decides what the host counts as. An unreachable reason (eysql_error:connect_failure/1) marks it down, read_only_server, a standby turned away by target_session_attrs => read_write, marks it a standby, and any other reason, such as a failed login, marks it rejected. Whichever it is, picks leave the host out until a refresh's probe, or a connection to it that opens, finds it back. The first failure of a run is logged with the reason, a standby at info and the others as warnings.

opened/3

opened(Cluster::cluster(), Reservation::reservation(), Conn::pid()) -> ok

The connection for Reservation is open; count it until its process exits.

pick/1

pick(Cluster::cluster()) -> {ok, eysql_topology:server(), connect_spec(), reservation()} | {error, pick_error()}

Reserve the best host for a new connection. The caller must follow with opened/3, open_failed/2 or cancel/2; connect/1 does. Until then the host counts as pending, unless the caller exits first. As pick/2 with nothing tried yet.

pick/2

pick(Cluster::cluster(), Tried::[eysql_topology:key()]) -> {ok, eysql_topology:server(), connect_spec(), reservation()} | {error, pick_error()}

As pick/1, leaving out the hosts in Tried, those the caller has tried already for this connection. {error, no_server_available} when no host is left that the mode allows; in a mode that refuses rather than fall back to the seeds, once servers are discovered, {error, {no_node_available, Type}} instead, Type being primary under only_primary, read_replica under only_rr, and cluster under fallback_to_topology_keys_only.

pick/3

pick(Cluster::cluster(), Tried::[eysql_topology:key()], Avoid::[eysql_topology:key()]) -> {ok, eysql_topology:server(), connect_spec(), reservation()} | {error, pick_error()}

As pick/2, choosing among the hosts it would choose among less those in Avoid, or among all of them when that leaves none. Avoid never changes which hosts those are: the tier, the topology level, the node type and, for the seeds and with load_balance false, the order stay what pick/2 uses. With load_balance false that is the one host that comes first, so Avoid changes nothing there. Nor is it a failure: the hosts are not marked, logged or probed.

refresh/1

refresh(Cluster::cluster()) -> ok

Refresh now: read yb_servers() again, unless on PostgreSQL, and probe the failed hosts whose delay has passed.

snapshot/1

snapshot(Cluster::cluster()) -> #{atom() => term()}

The cluster's current view.

start_link/1

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

Start a cluster process from a normalized config (eysql_config).

stop/1

stop(Cluster::cluster()) -> ok

terminate/2

terminate(Reason, State) -> any()

when_ready/1

when_ready(Cluster::cluster()) -> ready | pending

ready if the first discovery has finished (or there is none to do); otherwise pending, and the caller gets {eysql_cluster_ready, Cluster} when it finishes. open/1 waits for this, so that a pick follows the mode's rules for discovered servers, and a pool's first connections go to the servers rather than all to the seeds.


Generated by EDoc