Skip to content

Commit 35c1e47

Browse files
authored
feat(client): opt-in connection check on Qdrant::build() (#286)
* ci: add server-free unit-test workflow for connection check (#258) * 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. * 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.
1 parent 83a6c6d commit 35c1e47

2 files changed

Lines changed: 100 additions & 21 deletions

File tree

src/qdrant_client/config.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ pub struct QdrantConfig {
3939
/// Whether to check compatibility between the client and server versions
4040
pub check_compatibility: bool,
4141

42+
/// Whether to verify connectivity to the Qdrant server when building the client.
43+
///
44+
/// gRPC connections are established lazily, so by default building a client
45+
/// succeeds even when the server is unreachable; connection errors only surface
46+
/// on the first request. When enabled, [`build`](Self::build) performs a health
47+
/// check and returns an error if the server cannot be reached.
48+
pub check_connection: bool,
49+
4250
/// Amount of concurrent connections.
4351
/// If set to 0 or 1, connection pools will be disabled.
4452
pub pool_size: usize,
@@ -207,11 +215,37 @@ impl QdrantConfig {
207215
self
208216
}
209217

218+
/// Verify connectivity to the Qdrant server when building the client.
219+
///
220+
/// gRPC connections are established lazily, so [`build`](Self::build) normally
221+
/// succeeds even when the server is unreachable; connection failures would only
222+
/// be observed on the first API call. Enabling this makes `build` perform a
223+
/// health check and return an error if the server cannot be reached.
224+
///
225+
/// ```rust,no_run
226+
/// use qdrant_client::Qdrant;
227+
///
228+
/// let client = Qdrant::from_url("http://localhost:6334")
229+
/// .check_connection()
230+
/// .build();
231+
/// ```
232+
pub fn check_connection(mut self) -> Self {
233+
self.check_connection = true;
234+
self
235+
}
236+
210237
/// Set the pool size of concurrent connections.
211238
/// If set to 0 or 1, connection pools will be disabled.
212239
pub fn set_pool_size(&mut self, pool_size: usize) {
213240
self.pool_size = pool_size;
214241
}
242+
243+
/// Set whether to verify connectivity to the server when building the client.
244+
///
245+
/// Also see [`check_connection()`](fn@Self::check_connection).
246+
pub fn set_check_connection(&mut self, check_connection: bool) {
247+
self.check_connection = check_connection;
248+
}
215249
}
216250

217251
/// Default Qdrant client configuration.
@@ -227,6 +261,7 @@ impl Default for QdrantConfig {
227261
api_key: None,
228262
compression: None,
229263
check_compatibility: true,
264+
check_connection: false,
230265
pool_size: 3,
231266
custom_headers: Vec::new(),
232267
}

src/qdrant_client/mod.rs

Lines changed: 65 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -99,22 +99,22 @@ impl Qdrant {
9999
///
100100
/// Constructs the client and connects based on the given [`QdrantConfig`](config::QdrantConfig).
101101
pub fn new(config: QdrantConfig) -> QdrantResult<Self> {
102-
if config.check_compatibility {
103-
// create a temporary client to check compatibility
102+
if config.check_compatibility || config.check_connection {
103+
// create a temporary client to check connectivity and/or compatibility
104104
let channel = ChannelPool::new(
105105
config.uri.parse::<Uri>()?,
106106
config.timeout,
107107
config.connect_timeout,
108108
config.keep_alive_while_idle,
109-
1, // No need to create a pool for the compatibility check.
109+
1, // No need to create a pool for the health check.
110110
);
111111
let client = Self {
112112
channel: Arc::new(channel),
113113
config: config.clone(),
114114
};
115115

116116
// We're in sync context, spawn temporary runtime in thread to do async health check
117-
let server_version = thread::scope(|s| {
117+
let health_check = thread::scope(|s| {
118118
s.spawn(|| {
119119
tokio::runtime::Builder::new_current_thread()
120120
.enable_io()
@@ -125,24 +125,36 @@ impl Qdrant {
125125
})
126126
.join()
127127
.expect("Failed to join health check thread")
128-
})
129-
.ok()
130-
.map(|info| info.version);
131-
132-
let client_version = env!("CARGO_PKG_VERSION").to_string();
133-
if let Some(server_version) = server_version {
134-
let is_compatible = is_compatible(Some(&client_version), Some(&server_version));
135-
if !is_compatible {
136-
println!("Client version {client_version} is not compatible with server version {server_version}. \
137-
Major versions should match and minor version difference must not exceed 1. \
138-
Set check_compatibility=false to skip version check.");
128+
});
129+
130+
// When connection checking is requested, surface connection errors eagerly
131+
// instead of silently deferring them to the first API call.
132+
let server_version = match health_check {
133+
Ok(info) => Some(info.version),
134+
Err(err) => {
135+
if config.check_connection {
136+
return Err(err);
137+
}
138+
None
139+
}
140+
};
141+
142+
if config.check_compatibility {
143+
let client_version = env!("CARGO_PKG_VERSION").to_string();
144+
if let Some(server_version) = server_version {
145+
let is_compatible = is_compatible(Some(&client_version), Some(&server_version));
146+
if !is_compatible {
147+
println!("Client version {client_version} is not compatible with server version {server_version}. \
148+
Major versions should match and minor version difference must not exceed 1. \
149+
Set check_compatibility=false to skip version check.");
150+
}
151+
} else {
152+
println!(
153+
"Failed to obtain server version. \
154+
Unable to check client-server compatibility. \
155+
Set check_compatibility=false to skip version check."
156+
);
139157
}
140-
} else {
141-
println!(
142-
"Failed to obtain server version. \
143-
Unable to check client-server compatibility. \
144-
Set check_compatibility=false to skip version check."
145-
);
146158
}
147159
}
148160

@@ -253,3 +265,35 @@ impl Qdrant {
253265
.await
254266
}
255267
}
268+
269+
#[cfg(test)]
270+
mod tests {
271+
use std::time::Duration;
272+
273+
use super::*;
274+
275+
// Nothing listens on this port, so a connection attempt is refused quickly;
276+
// these tests do not require a running Qdrant server.
277+
const UNREACHABLE_URL: &str = "http://127.0.0.1:6999";
278+
279+
#[test]
280+
fn build_without_connection_check_succeeds_when_server_is_down() {
281+
// gRPC connects lazily; without a connection check, building the client
282+
// must succeed even when no server is reachable.
283+
let client = Qdrant::from_url(UNREACHABLE_URL)
284+
.skip_compatibility_check()
285+
.build();
286+
assert!(client.is_ok());
287+
}
288+
289+
#[test]
290+
fn build_with_connection_check_fails_when_server_is_down() {
291+
// With connection checking enabled, building must fail eagerly when the
292+
// server is unreachable (issue #258).
293+
let result = Qdrant::from_url(UNREACHABLE_URL)
294+
.check_connection()
295+
.connect_timeout(Duration::from_secs(1))
296+
.build();
297+
assert!(result.is_err());
298+
}
299+
}

0 commit comments

Comments
 (0)