settings: Support multiple database replicas in REMOTE_POSTGRES_HOST.

The libpq client library, used under the hood by psycopg2, supports
passing a list of hosts; they are tried sequentially until one of them
works[^1].

In cases where this is used, it is often the case that the other
servers are read-only hot spare replicas.  Since Zulip does not expect
to be in a read-only transaction, we require that the server that we
connect to be writable, by passing `target_session_attrs`[^2].

To limit how long we may block connecting to a potentially bad host
before moving on, we set `connection_timeout` from null (meaning
forever) to 2 (the lowest supported value)[^3], so we move on quickly
in the case that the server is running but unable to handle new
connections.

[^1]: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-MULTIPLE-HOSTS
[^2]: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-TARGET-SESSION-ATTRS
[^3]: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-CONNECT-TIMEOUT
This commit is contained in:
Alex Vandiver 2024-04-03 20:00:20 +00:00 committed by Tim Abbott
parent 92121a0626
commit bd82c6edf9
1 changed files with 7 additions and 0 deletions

View File

@ -280,6 +280,11 @@ DATABASES: Dict[str, Dict[str, Any]] = {
"OPTIONS": { "OPTIONS": {
"connection_factory": TimeTrackingConnection, "connection_factory": TimeTrackingConnection,
"cursor_factory": TimeTrackingCursor, "cursor_factory": TimeTrackingCursor,
# The default is null, which means no timeout; 2 is the
# minimum allowed value. We set this low, so we move on
# quickly, in the case that the server is running but
# unable to handle new connections for some reason.
"connect_timeout": 2,
}, },
} }
} }
@ -295,6 +300,8 @@ elif REMOTE_POSTGRES_HOST != "":
HOST=REMOTE_POSTGRES_HOST, HOST=REMOTE_POSTGRES_HOST,
PORT=REMOTE_POSTGRES_PORT, PORT=REMOTE_POSTGRES_PORT,
) )
if "," in REMOTE_POSTGRES_HOST:
DATABASES["default"]["OPTIONS"]["target_session_attrs"] = "read-write"
if get_secret("postgres_password") is not None: if get_secret("postgres_password") is not None:
DATABASES["default"].update( DATABASES["default"].update(
PASSWORD=get_secret("postgres_password"), PASSWORD=get_secret("postgres_password"),