diff --git a/CHANGES.rst b/CHANGES.rst index 5ea4df71..cc4e6b4f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,15 @@ Changes for crate ================= +Unreleased +========== + +- Added ``connect(..., probe=False)``, which creates a connection without + contacting the servers. The lowest server version is then resolved when it + is first read, and reading it raises ``ConnectionError`` while no server + responds. Use it when the cluster may not respond yet, for example in tests + and during startup. + 2026/09/17 2.3.0 ================ - Added ``DefaultTypeConverter`` support that decodes ``DataType.UUID`` diff --git a/docs/by-example/connection.rst b/docs/by-example/connection.rst index c678d079..e5ae3259 100644 --- a/docs/by-example/connection.rst +++ b/docs/by-example/connection.rst @@ -21,6 +21,13 @@ This section sets up a connection object, and inspects some of its attributes. >>> connection.lowest_server_version.version (2, 0, 0) +With ``probe=False``, the connection is created without contacting any server. +The lowest server version is then resolved when it is first read:: + + >>> connection = connect(client=ClientMocked(), probe=False) + >>> connection.lowest_server_version.version + (2, 0, 0) + cursor() ======== diff --git a/src/crate/client/connection.py b/src/crate/client/connection.py index e499ce14..ea2851ab 100644 --- a/src/crate/client/connection.py +++ b/src/crate/client/connection.py @@ -53,6 +53,7 @@ def __init__( time_zone=None, jwt_token=None, compress: Union[int, bool] = 8192, + probe: bool = True, ): """ :param servers: @@ -139,6 +140,13 @@ def __init__( ``False`` disables compression entirely. ``True`` compresses every request regardless of size. An integer compresses only when the payload exceeds that many bytes. + :param probe: + (optional, defaults to ``True``) + Contact the servers while the connection is created, and raise + ``ConnectionError`` when none of them responds. + With ``False``, creating the connection performs no request. The + server version is resolved when it is first read, and reading it + raises ``ConnectionError`` while no server responds. """ # noqa: E501 self._converter = converter @@ -168,9 +176,27 @@ def __init__( jwt_token=jwt_token, compress=compress, ) - self.lowest_server_version = self._lowest_server_version() + self._version_cache: Union[Version, None] = None + if probe: + self._version_cache = self._lowest_server_version() self._closed = False + @property + def lowest_server_version(self) -> Version: + """ + The lowest CrateDB version among the servers of this connection. + + With ``probe=False``, the servers are contacted on the first read, so + this raises ``ConnectionError`` when none of them responds. + """ + if self._version_cache is None: + self._version_cache = self._lowest_server_version() + return self._version_cache + + @lowest_server_version.setter + def lowest_server_version(self, version: Version) -> None: + self._version_cache = version + def cursor(self, **kwargs) -> Cursor: """ Return a new Cursor Object using the connection. @@ -208,7 +234,7 @@ def get_blob_container(self, container_name): """ return BlobContainer(container_name, self) - def _lowest_server_version(self): + def _lowest_server_version(self) -> Version: lowest = None servers = self.client.active_servers connection_errors = [] diff --git a/tests/client/test_connection.py b/tests/client/test_connection.py index 464ec123..f4dd8fae 100644 --- a/tests/client/test_connection.py +++ b/tests/client/test_connection.py @@ -65,6 +65,31 @@ def test_lowest_server_version(): assert (1, 0, 3) == connection.lowest_server_version.version +def test_connect_without_probe_does_not_need_a_server(): + """ + Verify `probe=False` creates a connection without contacting any server. + """ + client = Client(servers="localhost:1234") + connection = connect(client=client, probe=False) + + with pytest.raises(crate.client.exceptions.ConnectionError) as excinfo: + connection.lowest_server_version # noqa: B018 + assert excinfo.match("Server not available") + + +def test_lowest_server_version_resolved_on_first_read(): + """ + Verify the server version is resolved when read, not when connecting. + """ + client = Client(servers="localhost:4200") + client.server_infos = lambda server: (None, None, "5.5.2") + + connection = connect(client=client, probe=False) + assert connection._version_cache is None + assert (5, 5, 2) == connection.lowest_server_version.version + assert (5, 5, 2) == connection._version_cache.version + + def test_connection_closes_access(): """ Verify that a connection closes on exit and that it also closes