This is an automated email from the ASF dual-hosted git repository.

janhoy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr-orbit.git


The following commit(s) were added to refs/heads/main by this push:
     new f966dabd Pass the configured timeout to the data client (#117)
f966dabd is described below

commit f966dabd7c77c368971d62e0151473efab542084
Author: Jan Høydahl <[email protected]>
AuthorDate: Fri Oct 16 15:41:13 2026 +0200

    Pass the configured timeout to the data client (#117)
---
 solrorbit/client.py            | 42 ++++++++++++++++++++++++++++++++++++++--
 tests/unit/solr/test_client.py | 44 ++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 84 insertions(+), 2 deletions(-)

diff --git a/solrorbit/client.py b/solrorbit/client.py
index 3724cfe4..7f24d244 100644
--- a/solrorbit/client.py
+++ b/solrorbit/client.py
@@ -42,6 +42,14 @@ logger = logging.getLogger(__name__)
 # Exceptions
 # ---------------------------------------------------------------------------
 
+#: Socket read timeout used when ``--client-options`` does not set one.
+#:
+#: Fine for searches and bulk updates. Deliberately *not* relied on for
+#: long single operations such as ``optimize``, which a caller is expected to
+#: raise with ``--client-options=timeout:N``.
+DEFAULT_CLIENT_TIMEOUT = 30
+
+
 class SolrClientError(Exception):
     """Base exception for all SolrAdminClient errors."""
 
@@ -71,7 +79,7 @@ class SolrAdminClient:
 
     def __init__(self, host: str, port: int = 8983,
                  username: str = None, password: str = None,
-                 tls: bool = False, timeout: int = 30):
+                 tls: bool = False, timeout: int = DEFAULT_CLIENT_TIMEOUT):
         scheme = "https" if tls else "http"
         self.base_url = f"{scheme}://{host}:{port}"
         self.api_url = f"{self.base_url}/api"
@@ -476,7 +484,7 @@ class SolrClient(RequestContextHolder):  # pylint: 
disable=too-many-public-metho
             pass
 
     def __init__(self, host="localhost", port=8983, username=None, 
password=None,
-                 tls=False, timeout=30):
+                 tls=False, timeout=DEFAULT_CLIENT_TIMEOUT):
         self._host = host
         self._port = port
         self._username = username
@@ -614,7 +622,37 @@ class ClientFactory:
             username=self._client_options.get("basic_auth_user"),
             password=self._client_options.get("basic_auth_password"),
             tls=self._client_options.get("use_ssl", False),
+            timeout=self._read_timeout(),
         )
 
+    def _read_timeout(self) -> int:
+        """Socket read timeout for every request this client makes, in seconds.
+
+        ``--client-options=timeout:N`` used to be dropped here, so the client
+        always ran on the 30-second constructor default no matter what was
+        asked for. That is survivable for searches and bulk updates and fatal
+        for ``optimize``: a force-merge of a 165M-document index is a single
+        HTTP request that runs for many minutes, and it failed at 30 seconds
+        having already done the work -- after, in one observed case, 86
+        minutes of successful indexing.
+
+        Invalid input falls back to the default rather than failing the run:
+        the alternative is aborting a benchmark over a typo in a tuning knob.
+        """
+        raw = self._client_options.get("timeout", DEFAULT_CLIENT_TIMEOUT)
+        try:
+            timeout = int(raw)
+        except (TypeError, ValueError):
+            self.logger.warning(
+                "Ignoring client option timeout=%r, which is not a number; 
using %ds",
+                raw, DEFAULT_CLIENT_TIMEOUT)
+            return DEFAULT_CLIENT_TIMEOUT
+        if timeout <= 0:
+            self.logger.warning(
+                "Ignoring client option timeout=%d, which is not positive; 
using %ds",
+                timeout, DEFAULT_CLIENT_TIMEOUT)
+            return DEFAULT_CLIENT_TIMEOUT
+        return timeout
+
     def create_async(self):
         return self.create()
diff --git a/tests/unit/solr/test_client.py b/tests/unit/solr/test_client.py
index bf2e2216..ed512e82 100644
--- a/tests/unit/solr/test_client.py
+++ b/tests/unit/solr/test_client.py
@@ -294,3 +294,47 @@ class TestSolrAdminClientIsCloudMode(unittest.TestCase):
 
 if __name__ == "__main__":
     unittest.main()
+
+
+class TestClientFactoryTimeout(unittest.TestCase):
+    """--client-options=timeout:N has to reach the client that uses it.
+
+    It used to be dropped in ClientFactory.create(), so every data operation
+    ran on the 30-second constructor default. Survivable for searches and
+    bulk updates; fatal for optimize, which is one HTTP request that
+    force-merges the whole index. A full nyc_taxis run indexed 165,346,692
+    documents over 86 minutes and then failed there at 30 seconds, having
+    already done the work.
+    """
+
+    @staticmethod
+    def _factory(**options):
+        from solrorbit.client import ClientFactory
+        return ClientFactory([{"host": "localhost", "port": 8983}], options)
+
+    def test_the_configured_timeout_reaches_the_client(self):
+        assert self._factory(timeout=7200).create()._timeout == 7200
+
+    def test_it_also_reaches_the_pysolr_data_client(self):
+        # optimize goes through pysolr, not the admin client, which is why
+        # dropping it here was invisible until a force-merge took minutes.
+        client = self._factory(timeout=1234).create()
+        assert client._get_pysolr("c").timeout == 1234
+
+    def test_and_the_admin_client(self):
+        assert self._factory(timeout=1234).create()._admin.timeout == 1234
+
+    def test_a_string_is_accepted(self):
+        # client_options arrive parsed from a comma-separated CLI string.
+        assert self._factory(timeout="900").create()._timeout == 900
+
+    def test_the_default_applies_when_unset(self):
+        from solrorbit.client import DEFAULT_CLIENT_TIMEOUT
+        assert self._factory().create()._timeout == DEFAULT_CLIENT_TIMEOUT
+
+    def test_nonsense_falls_back_rather_than_failing_the_run(self):
+        # Aborting a benchmark over a typo in a tuning knob is worse than
+        # running it on the default.
+        from solrorbit.client import DEFAULT_CLIENT_TIMEOUT
+        assert self._factory(timeout="soon").create()._timeout == 
DEFAULT_CLIENT_TIMEOUT
+        assert self._factory(timeout=-5).create()._timeout == 
DEFAULT_CLIENT_TIMEOUT

Reply via email to