Require SolrCloud mode: enforce at runtime and document clearly (#43)
diff --git a/docs/quickstart.md b/docs/quickstart.md
index 247d47e..2643d88 100644
--- a/docs/quickstart.md
+++ b/docs/quickstart.md
@@ -17,10 +17,14 @@
## Set up a Solr cluster
+{: .important }
+Solr Orbit requires Solr to run in **SolrCloud mode**. Standalone/user-managed mode is not supported. For Solr 9.x, start Solr with the `-c` flag to enable SolrCloud mode. For Solr 10.0.0 and later, SolrCloud is the default and no extra flag is required.
+
If you don't already have a running Solr cluster, the easiest way to start one is with Docker:
```bash
-docker run -d --name solr-orbit -p 8983:8983 solr:9 solr-demo
+# Solr 9.x — the -c flag enables SolrCloud mode
+docker run -d --name solr-orbit -p 8983:8983 solr:9 -c
```
Verify that Solr is running by opening [http://localhost:8983/solr/](http://localhost:8983/solr/) in your browser, or with:
diff --git a/docs/reference/workloads/test-procedures.md b/docs/reference/workloads/test-procedures.md
index 8cf439b..a4db6b9 100644
--- a/docs/reference/workloads/test-procedures.md
+++ b/docs/reference/workloads/test-procedures.md
@@ -60,6 +60,9 @@
## Selecting a test procedure at run time
+{: .important }
+The target Solr cluster must be running in **SolrCloud (ZooKeeper) mode**. For Solr 9.x, start with `-c` (e.g. `docker run -d -p 8983:8983 solr:9 -c`). For Solr 10.0.0+, SolrCloud is the default.
+
```bash
solr-orbit run \
--pipeline benchmark-only \
diff --git a/docs/user-guide/working-with-workloads/running-workloads.md b/docs/user-guide/working-with-workloads/running-workloads.md
index bb7eb2e..3af8fd3 100644
--- a/docs/user-guide/working-with-workloads/running-workloads.md
+++ b/docs/user-guide/working-with-workloads/running-workloads.md
@@ -29,6 +29,9 @@
[--workload WORKLOAD | --workload-path PATH] [OPTIONS]
```
+{: .important }
+When using the `benchmark-only` pipeline, the target Solr cluster must be running in **SolrCloud (ZooKeeper) mode** — standalone/user-managed mode is not supported. For Solr 9.x, start Solr with the `-c` flag (e.g. `bin/solr start -c` or `docker run -d -p 8983:8983 solr:9 -c`). For Solr 10.0.0 and later, SolrCloud is the default.
+
---
## Using a named workload
diff --git a/solrorbit/client.py b/solrorbit/client.py
index b872003..7353871 100644
--- a/solrorbit/client.py
+++ b/solrorbit/client.py
@@ -330,6 +330,38 @@
return resp.text
return resp.json()
+ def is_cloud_mode(self) -> bool:
+ """
+ Return True if this Solr node is running in SolrCloud (ZooKeeper) mode.
+
+ Calls the Collections CLUSTERSTATUS API, which only succeeds in cloud mode.
+ Returns False if Solr responds with a 400 indicating standalone/user-managed mode.
+ Raises SolrClientError on connection errors or unexpected failures.
+ """
+ try:
+ resp = self._get_session().get(
+ f"{self.base_url}/solr/admin/collections",
+ params={"action": "CLUSTERSTATUS", "wt": "json"},
+ timeout=self.timeout,
+ )
+ except requests.exceptions.RequestException as exc:
+ raise SolrClientError(
+ f"Could not connect to Solr at {self.base_url}: {exc}"
+ ) from exc
+ if resp.ok:
+ return True
+ # HTTP 400 with a message about SolrCloud/user-managed → definitely standalone mode
+ body = self._try_parse_json(resp)
+ msg = str(body.get("error", {}).get("msg", "")).lower()
+ if resp.status_code == 400 and any(
+ kw in msg for kw in ("solrcloud", "user-managed", "collections api")
+ ):
+ return False
+ # Auth error, unexpected status, etc. — surface to caller
+ raise SolrClientError(
+ f"Unexpected response from CLUSTERSTATUS (HTTP {resp.status_code}): {resp.text[:300]}"
+ )
+
# ------------------------------------------------------------------
# Raw request (for the raw-request workload operation)
# ------------------------------------------------------------------
@@ -478,6 +510,9 @@
def wait_for_cluster_ready(self, **kwargs):
return self._admin.wait_for_cluster_ready(**kwargs)
+ def is_cloud_mode(self) -> bool:
+ return self._admin.is_cloud_mode()
+
def raw_request(self, method, path, body=None, headers=None):
return self._admin.raw_request(method, path, body=body, headers=headers)
diff --git a/solrorbit/test_run_orchestrator.py b/solrorbit/test_run_orchestrator.py
index 7ff1af4..9b6f02c 100644
--- a/solrorbit/test_run_orchestrator.py
+++ b/solrorbit/test_run_orchestrator.py
@@ -377,9 +377,45 @@
cfg.add(config.Scope.benchmark, "client", "hosts", default_host_object)
+def _check_cloud_mode(cfg):
+ """Fail fast with a clear error if the target Solr cluster is not in SolrCloud mode."""
+ from solrorbit.client import SolrAdminClient, SolrClientError # local import to avoid circular
+
+ configured_hosts = cfg.opts("client", "hosts")
+ hosts = configured_hosts.default
+ if not hosts:
+ return
+
+ entry = hosts[0]
+ if isinstance(entry, dict):
+ host = entry.get("host", "localhost")
+ port = int(entry.get("port", 8983))
+ else:
+ parts = str(entry).rsplit(":", 1)
+ host = parts[0]
+ port = int(parts[1]) if len(parts) == 2 and parts[1].isdigit() else 8983
+
+ client = SolrAdminClient(host=host, port=port)
+ try:
+ if not client.is_cloud_mode():
+ raise exceptions.SystemSetupError(
+ f"Solr at {host}:{port} is not running in SolrCloud (ZooKeeper) mode.\n"
+ "Solr Orbit currently only supports SolrCloud mode.\n"
+ " \u2022 For Solr 9.x: start with the '-c' flag, e.g.:\n"
+ " docker run -d -p 8983:8983 solr:9 -c\n"
+ " bin/solr start -c\n"
+ " \u2022 For Solr 10.0.0+: SolrCloud is the default; no extra flag is needed."
+ )
+ except SolrClientError as exc:
+ raise exceptions.SystemSetupError(
+ f"Could not verify Solr mode at {host}:{port}: {exc}"
+ ) from exc
+
+
# Poor man's curry
def benchmark_only(cfg):
set_default_hosts(cfg)
+ _check_cloud_mode(cfg)
return run_test(cfg, external=True)
diff --git a/tests/unit/solr/test_client.py b/tests/unit/solr/test_client.py
index c89ddf8..bf2e221 100644
--- a/tests/unit/solr/test_client.py
+++ b/tests/unit/solr/test_client.py
@@ -241,5 +241,56 @@
self.assertTrue(any("solrconfig.xml" in n for n in names))
+class TestSolrAdminClientIsCloudMode(unittest.TestCase):
+ def _make_client_with_mock_session(self):
+ client = SolrAdminClient("localhost")
+ client._session = MagicMock()
+ return client
+
+ def test_returns_true_when_clusterstatus_succeeds(self):
+ client = self._make_client_with_mock_session()
+ resp = _make_response(status_code=200, json_data={"cluster": {"collections": {}}})
+ client._session.get.return_value = resp
+ self.assertTrue(client.is_cloud_mode())
+
+ def test_returns_false_when_solrcloud_not_running(self):
+ client = self._make_client_with_mock_session()
+ resp = _make_response(
+ status_code=400,
+ json_data={"error": {"msg": "Solr instance is not running in SolrCloud mode."}},
+ text='{"error": {"msg": "Solr instance is not running in SolrCloud mode."}}',
+ )
+ client._session.get.return_value = resp
+ self.assertFalse(client.is_cloud_mode())
+
+ def test_returns_false_when_user_managed_mode(self):
+ client = self._make_client_with_mock_session()
+ resp = _make_response(
+ status_code=400,
+ json_data={"error": {"msg": "Collections API is disabled in user-managed mode."}},
+ text='{"error": {"msg": "Collections API is disabled in user-managed mode."}}',
+ )
+ client._session.get.return_value = resp
+ self.assertFalse(client.is_cloud_mode())
+
+ def test_raises_on_unexpected_error_status(self):
+ client = self._make_client_with_mock_session()
+ resp = _make_response(
+ status_code=503,
+ json_data={},
+ text="Service Unavailable",
+ )
+ client._session.get.return_value = resp
+ with self.assertRaises(SolrClientError):
+ client.is_cloud_mode()
+
+ def test_raises_on_connection_error(self):
+ import requests as _requests
+ client = self._make_client_with_mock_session()
+ client._session.get.side_effect = _requests.exceptions.ConnectionError("refused")
+ with self.assertRaises(SolrClientError):
+ client.is_cloud_mode()
+
+
if __name__ == "__main__":
unittest.main()