From 7776b80e522db4ac67dba8c0893a8c76a2aa1095 Mon Sep 17 00:00:00 2001 From: ameyypawar Date: Tue, 30 Jun 2026 22:50:40 +0530 Subject: [PATCH 1/3] ci: add server-free unit-test workflow for connection check (#258) --- .github/workflows/test.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6ca6c05..51dcb98 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,19 +1,19 @@ -name: Test +name: Unit tests on: [ push ] jobs: - build: + unit-tests: + runs-on: ubuntu-latest + steps: - name: Install minimal stable uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - name: Install Protoc uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Run integration tests - run: | - ./tests/integration-tests.sh - shell: bash + - name: Run unit tests + run: cargo test --lib --all-features From ff62f58a18f86e957543336adae06610285dd1f0 Mon Sep 17 00:00:00 2001 From: Amey Pawar <138877912+ameyypawar@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:26:56 +0530 Subject: [PATCH 2/3] feat(client): add opt-in connection check on build (#258) gRPC connections are established lazily, so `Qdrant::build()` returns `Ok` even when the server is unreachable; connection errors only surface on the first API call. The default compatibility check already performs a health check on build, but silently discards connection failures. Add an opt-in `check_connection` config flag with a `check_connection()` builder method and a `set_check_connection()` setter. When enabled, `build()` reuses the existing health-check path and returns the error eagerly instead of deferring it. Default behavior is unchanged. Add server-free unit tests for both the lazy (default) and eager (opt-in) build paths. The CI workflow that runs them was added in the preceding commit. --- src/qdrant_client/config.rs | 35 +++++++++++++++ src/qdrant_client/mod.rs | 86 ++++++++++++++++++++++++++++--------- 2 files changed, 100 insertions(+), 21 deletions(-) diff --git a/src/qdrant_client/config.rs b/src/qdrant_client/config.rs index f68467d..3119dd4 100644 --- a/src/qdrant_client/config.rs +++ b/src/qdrant_client/config.rs @@ -39,6 +39,14 @@ pub struct QdrantConfig { /// Whether to check compatibility between the client and server versions pub check_compatibility: bool, + /// Whether to verify connectivity to the Qdrant server when building the client. + /// + /// gRPC connections are established lazily, so by default building a client + /// succeeds even when the server is unreachable; connection errors only surface + /// on the first request. When enabled, [`build`](Self::build) performs a health + /// check and returns an error if the server cannot be reached. + pub check_connection: bool, + /// Amount of concurrent connections. /// If set to 0 or 1, connection pools will be disabled. pub pool_size: usize, @@ -207,11 +215,37 @@ impl QdrantConfig { self } + /// Verify connectivity to the Qdrant server when building the client. + /// + /// gRPC connections are established lazily, so [`build`](Self::build) normally + /// succeeds even when the server is unreachable; connection failures would only + /// be observed on the first API call. Enabling this makes `build` perform a + /// health check and return an error if the server cannot be reached. + /// + /// ```rust,no_run + /// use qdrant_client::Qdrant; + /// + /// let client = Qdrant::from_url("http://localhost:6334") + /// .check_connection() + /// .build(); + /// ``` + pub fn check_connection(mut self) -> Self { + self.check_connection = true; + self + } + /// Set the pool size of concurrent connections. /// If set to 0 or 1, connection pools will be disabled. pub fn set_pool_size(&mut self, pool_size: usize) { self.pool_size = pool_size; } + + /// Set whether to verify connectivity to the server when building the client. + /// + /// Also see [`check_connection()`](fn@Self::check_connection). + pub fn set_check_connection(&mut self, check_connection: bool) { + self.check_connection = check_connection; + } } /// Default Qdrant client configuration. @@ -227,6 +261,7 @@ impl Default for QdrantConfig { api_key: None, compression: None, check_compatibility: true, + check_connection: false, pool_size: 3, custom_headers: Vec::new(), } diff --git a/src/qdrant_client/mod.rs b/src/qdrant_client/mod.rs index ac9a4a4..6376037 100644 --- a/src/qdrant_client/mod.rs +++ b/src/qdrant_client/mod.rs @@ -99,14 +99,14 @@ impl Qdrant { /// /// Constructs the client and connects based on the given [`QdrantConfig`](config::QdrantConfig). pub fn new(config: QdrantConfig) -> QdrantResult { - if config.check_compatibility { - // create a temporary client to check compatibility + if config.check_compatibility || config.check_connection { + // create a temporary client to check connectivity and/or compatibility let channel = ChannelPool::new( config.uri.parse::()?, config.timeout, config.connect_timeout, config.keep_alive_while_idle, - 1, // No need to create a pool for the compatibility check. + 1, // No need to create a pool for the health check. ); let client = Self { channel: Arc::new(channel), @@ -114,7 +114,7 @@ impl Qdrant { }; // We're in sync context, spawn temporary runtime in thread to do async health check - let server_version = thread::scope(|s| { + let health_check = thread::scope(|s| { s.spawn(|| { tokio::runtime::Builder::new_current_thread() .enable_io() @@ -125,24 +125,36 @@ impl Qdrant { }) .join() .expect("Failed to join health check thread") - }) - .ok() - .map(|info| info.version); - - let client_version = env!("CARGO_PKG_VERSION").to_string(); - if let Some(server_version) = server_version { - let is_compatible = is_compatible(Some(&client_version), Some(&server_version)); - if !is_compatible { - println!("Client version {client_version} is not compatible with server version {server_version}. \ - Major versions should match and minor version difference must not exceed 1. \ - Set check_compatibility=false to skip version check."); + }); + + // When connection checking is requested, surface connection errors eagerly + // instead of silently deferring them to the first API call. + let server_version = match health_check { + Ok(info) => Some(info.version), + Err(err) => { + if config.check_connection { + return Err(err); + } + None + } + }; + + if config.check_compatibility { + let client_version = env!("CARGO_PKG_VERSION").to_string(); + if let Some(server_version) = server_version { + let is_compatible = is_compatible(Some(&client_version), Some(&server_version)); + if !is_compatible { + println!("Client version {client_version} is not compatible with server version {server_version}. \ + Major versions should match and minor version difference must not exceed 1. \ + Set check_compatibility=false to skip version check."); + } + } else { + println!( + "Failed to obtain server version. \ + Unable to check client-server compatibility. \ + Set check_compatibility=false to skip version check." + ); } - } else { - println!( - "Failed to obtain server version. \ - Unable to check client-server compatibility. \ - Set check_compatibility=false to skip version check." - ); } } @@ -253,3 +265,35 @@ impl Qdrant { .await } } + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use super::*; + + // Nothing listens on this port, so a connection attempt is refused quickly; + // these tests do not require a running Qdrant server. + const UNREACHABLE_URL: &str = "http://127.0.0.1:6999"; + + #[test] + fn build_without_connection_check_succeeds_when_server_is_down() { + // gRPC connects lazily; without a connection check, building the client + // must succeed even when no server is reachable. + let client = Qdrant::from_url(UNREACHABLE_URL) + .skip_compatibility_check() + .build(); + assert!(client.is_ok()); + } + + #[test] + fn build_with_connection_check_fails_when_server_is_down() { + // With connection checking enabled, building must fail eagerly when the + // server is unreachable (issue #258). + let result = Qdrant::from_url(UNREACHABLE_URL) + .check_connection() + .connect_timeout(Duration::from_secs(1)) + .build(); + assert!(result.is_err()); + } +} From 33dc333913ba9130df0194e85498457e062f1dab Mon Sep 17 00:00:00 2001 From: Amey Pawar <138877912+ameyypawar@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:41:28 +0530 Subject: [PATCH 3/3] ci: restore upstream test.yml (drop redundant unit-test workflow) The existing Test workflow already runs `cargo test --all` via integration-tests.sh, which covers the new unit tests. Revert the accidental change to this file. --- .github/workflows/test.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 51dcb98..6ca6c05 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,19 +1,19 @@ -name: Unit tests +name: Test on: [ push ] jobs: - unit-tests: - + build: runs-on: ubuntu-latest - steps: - name: Install minimal stable uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Protoc uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Run unit tests - run: cargo test --lib --all-features + - name: Run integration tests + run: | + ./tests/integration-tests.sh + shell: bash