diff --git a/CHANGES.rst b/CHANGES.rst index d2afd0ad..446dd3a1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -28,6 +28,8 @@ Changes for crate ``%(name)s`` placeholders and converts them to positional ``?`` markers client-side. Positional parameters using ``?`` continue to work unchanged. +- Changed connection behaviour to fail early if the database cluster does not respond + 2026/03/09 2.1.2 ================ diff --git a/docs/by-example/client.rst b/docs/by-example/client.rst index 995ee745..52897375 100644 --- a/docs/by-example/client.rst +++ b/docs/by-example/client.rst @@ -29,12 +29,8 @@ respond, the request is automatically routed to the next server: >>> connection = client.connect([invalid_host, crate_host]) >>> connection.close() -If no ``servers`` are given, the default one ``http://127.0.0.1:4200`` is used: - - >>> connection = client.connect() - >>> connection.client._active_servers - ['http://127.0.0.1:4200'] - >>> connection.close() +If no ``servers`` are supplied to the ``connect`` method, the default address +``http://127.0.0.1:4200`` is used. If the option ``error_trace`` is set to ``True``, the client will print a whole traceback if a server error occurs: @@ -77,7 +73,7 @@ connect: The username for trusted users can also be provided in the URL: - >>> connection = client.connect(['http://trusted_me@' + crate_host]) + >>> connection = client.connect([crate_host.replace('://', '://trusted_me@')]) >>> connection.client.username 'trusted_me' >>> connection.client.password @@ -97,7 +93,7 @@ also need to provide ``password`` as argument for the ``connect()`` call: The authentication credentials can also be provided in the URL: - >>> connection = client.connect(['http://me:my_secret_pw@' + crate_host]) + >>> connection = client.connect([crate_host.replace('://', '://me:my_secret_pw@')]) >>> connection.client.username 'me' >>> connection.client.password diff --git a/src/crate/client/connection.py b/src/crate/client/connection.py index f722b848..17517c6f 100644 --- a/src/crate/client/connection.py +++ b/src/crate/client/connection.py @@ -18,10 +18,11 @@ # However, if you have executed another commercial license agreement # with Crate these terms will supersede the license and you may use the # software solely pursuant to the terms of the relevant commercial agreement. - +import json from typing import Union from verlib2 import Version +from verlib2.packaging.version import InvalidVersion from .blob import BlobContainer from .cursor import Cursor @@ -211,14 +212,21 @@ def get_blob_container(self, container_name): def _lowest_server_version(self): lowest = None + server_count = len(self.client.active_servers) + connection_errors = [] for server in self.client.active_servers: try: _, _, version = self.client.server_infos(server) version = Version(version) - except (ValueError, ConnectionError): + except ConnectionError as ex: + connection_errors.append(ex) + continue + except (ValueError, InvalidVersion): continue if not lowest or version < lowest: lowest = version + if connection_errors and len(connection_errors) == server_count: + raise ConnectionError(json.dumps(list(map(str, connection_errors)))) return lowest or Version("0.0.0") def __repr__(self): diff --git a/tests/client/test_connection.py b/tests/client/test_connection.py index 90b121f2..c64c65e2 100644 --- a/tests/client/test_connection.py +++ b/tests/client/test_connection.py @@ -4,6 +4,7 @@ import pytest from urllib3 import Timeout +import crate.client.exceptions from crate.client import connect from crate.client.connection import Connection from crate.client.exceptions import ProgrammingError @@ -12,6 +13,13 @@ from .settings import crate_host +def test_invalid_server_address(): + client = Client(servers="localhost:4202") + with pytest.raises(crate.client.exceptions.ConnectionError) as excinfo: + connect(client=client) + assert excinfo.match("Server not available") + + def test_lowest_server_version(): """ Verify the lowest server version is correctly set. @@ -55,10 +63,13 @@ def test_connection_closes_access(): def test_connection_closes_context_manager(): """Verify that the context manager of the client closes the connection""" - with patch.object(connect, "close", autospec=True) as close_fn: - with connect(): - pass - close_fn.assert_called_once() + with patch.object( + Client, "server_infos", return_value=(None, None, "0.0.0") + ): + with patch.object(connect, "close", autospec=True) as close_fn: + with connect(): + pass + close_fn.assert_called_once() def test_invalid_server_version(): @@ -78,8 +89,11 @@ def test_context_manager(): """ close_method = "crate.client.http.Client.close" with patch(close_method, return_value=MagicMock()) as close_func: - with connect("localhost:4200") as conn: - assert not conn._closed + with patch.object( + Client, "server_infos", return_value=(None, None, "0.0.0") + ): + with connect("localhost:4200") as conn: + assert not conn._closed assert conn._closed # Checks that the close method of the client @@ -115,7 +129,10 @@ def test_default_repr(): """ Verify default repr dunder method. """ - conn = connect() + with patch.object( + Client, "server_infos", return_value=(None, None, "0.0.0") + ): + conn = connect() assert repr(conn) == ">" @@ -132,7 +149,10 @@ def test_with_timezone(): """ tz_mst = datetime.timezone(datetime.timedelta(hours=7), name="MST") - connection = connect("localhost:4200", time_zone=tz_mst) + with patch.object( + Client, "server_infos", return_value=(None, None, "0.0.0") + ): + connection = connect("localhost:4200", time_zone=tz_mst) cursor = connection.cursor() assert cursor.time_zone.tzname(None) == "MST" @@ -148,16 +168,22 @@ def test_timeout_float(): """ Verify setting the timeout value as a scalar (float) works. """ - with connect("localhost:4200", timeout=2.42) as conn: - assert conn.client._pool_kw["timeout"] == 2.42 + with patch.object( + Client, "server_infos", return_value=(None, None, "0.0.0") + ): + with connect("localhost:4200", timeout=2.42) as conn: + assert conn.client._pool_kw["timeout"] == 2.42 def test_timeout_string(): """ Verify setting the timeout value as a scalar (string) works. """ - with connect("localhost:4200", timeout="2.42") as conn: - assert conn.client._pool_kw["timeout"] == 2.42 + with patch.object( + Client, "server_infos", return_value=(None, None, "0.0.0") + ): + with connect("localhost:4200", timeout="2.42") as conn: + assert conn.client._pool_kw["timeout"] == 2.42 def test_timeout_object(): @@ -165,5 +191,8 @@ def test_timeout_object(): Verify setting the timeout value as a Timeout object works. """ timeout = Timeout(connect=2.42, read=0.01) - with connect("localhost:4200", timeout=timeout) as conn: - assert conn.client._pool_kw["timeout"] == timeout + with patch.object( + Client, "server_infos", return_value=(None, None, "0.0.0") + ): + with connect("localhost:4200", timeout=timeout) as conn: + assert conn.client._pool_kw["timeout"] == timeout diff --git a/tests/client/test_http.py b/tests/client/test_http.py index 573fca68..3510de41 100644 --- a/tests/client/test_http.py +++ b/tests/client/test_http.py @@ -655,7 +655,14 @@ def do_POST(self): time.sleep(timeout + 0.1) def do_GET(self): - pass + body = json.dumps( + {"name": "test", "version": {"number": "0.0.0"}} + ).encode() + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) # Start the http server. with serve_http(TimeoutRequestHandler) as (server, url): @@ -710,7 +717,14 @@ def do_POST(self): self.wfile.write(response.encode("utf-8")) def do_GET(self): - pass + body = json.dumps( + {"name": "test", "version": {"number": "0.0.0"}} + ).encode() + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) def test_default_schema(serve_http):