Skip to content

Add load_balance_hosts to spread connections across multiple hosts - #1394

Open
lesnik512 wants to merge 1 commit into
MagicStack:masterfrom
lesnik512:load-balance-hosts
Open

lesnik512 wants to merge 1 commit into
MagicStack:masterfrom
lesnik512:load-balance-hosts

Conversation

@lesnik512

Copy link
Copy Markdown

With several hosts, connect() always tries them in the order given. Every connection, including every connection a pool opens, goes to the first host that accepts it. With target_session_attrs="read-only" or "prefer-standby" and several replicas, the first replica in the list gets all the load and the others sit idle.

This PR adds load_balance_hosts, the libpq option of the same name (PostgreSQL 16+):

  • "disable" (default): hosts are tried in the order listed, as today.
  • "random": on every connect() call the address list is shuffled before the usual host loop runs. Failover and target_session_attrs work as before; only the order changes. A pool calls connect() for each new connection, so pool connections end up spread across the hosts.

The value is resolved like target_session_attrs: keyword argument, then load_balance_hosts in the DSN, then the service file, then PGLOADBALANCEHOSTS, then "disable". Other values raise ClientConfigurationError. A load_balance_hosts DSN option is no longer sent to the server as a setting. Today it fails with UndefinedObjectError.

Why in asyncpg:

  • Callers can't shuffle the hosts once and pass them in, because the order has to change for every connection. Today that takes a custom connect= for create_pool(), or an async_creator for SQLAlchemy's asyncpg dialect, that reorders the hosts before each call. Downstream projects already do this (for example modern-python/db-retry).
  • The name, values, DSN key and environment variable are libpq's, the same way asyncpg already handles target_session_attrs, service, krbsrvname and gsslib. A DSN that load-balances with psql and psycopg works the same here.

libpq also shuffles the addresses a single host name resolves to. asyncpg leaves DNS resolution to the event loop's create_connection(), so this PR shuffles only the (host, port) entries.

Tests:

  • TestConnectParams: default, DSN, keyword argument, environment variable, service file and invalid values.
  • TestLoadBalanceHosts: with random the hosts are tried in the shuffled order and the order is drawn again on each connect(). With the default or disable, the listed order is kept and random isn't called.
  • Full suite passes on Python 3.13 against PostgreSQL 18, and tests.test_connect on Python 3.10. flake8 and mypy are clean.

With several hosts, connect() always tries them in the listed order, so
every connection, including each one a pool opens, lands on the first
host that accepts it. load_balance_hosts="random" shuffles the address
list on every connect() call, like libpq's option of the same name.
The default "disable" keeps the listed order.

The value is read from the keyword argument, the DSN, the service file
or PGLOADBALANCEHOSTS. A load_balance_hosts DSN option is no longer
sent to the server as a setting.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant