Skip to content

Commit 63abae2

Browse files
claudespiceClaudegrokspice
authored
feat: add runtime_status and is_ready (#81)
* feat: add runtime_status and is_ready Adds SpiceClient::runtime_status (GET /v1/status), reporting per-component state for http, flight, metrics and opentelemetry, and SpiceClient::is_ready (GET /v1/ready) for the boolean case. Neither was reachable from the SDK. ComponentStatus keeps an Other(String) variant so a status added by a newer runtime deserializes rather than failing. * fix: name the failing endpoint in StatusError messages StatusError::RequestFailed and ::HttpError are raised by both runtime_status (GET /v1/status) and is_ready (GET /v1/ready), but both displayed "Failed to get runtime status", which is misleading in a readiness probe. Carry the URL and name it in the message instead. --------- Co-authored-by: claudespice <270518434+claudespice@users.noreply.github.com> Co-authored-by: Claude <claude@Claudes-Mini.localdomain> Co-authored-by: grokspice <grokspice@spice.ai>
1 parent 97add5e commit 63abae2

5 files changed

Lines changed: 371 additions & 0 deletions

File tree

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,43 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
210210

211211
To cancel an *async query job* instead, use `cancel_query()` — see [Async query jobs](#async-query-jobs-and-dataset-refresh) above.
212212

213+
### Runtime health and status
214+
215+
`is_ready()` is a single boolean for the whole runtime. When you need to know *which*
216+
component is not ready, `runtime_status()` reports each connection separately. Both use
217+
the HTTP API, so configure `http_url()`.
218+
219+
```rust,no_run
220+
use spiceai::ClientBuilder;
221+
222+
#[tokio::main]
223+
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
224+
let client = ClientBuilder::new()
225+
.http_url("http://localhost:8090")
226+
.build()
227+
.await?;
228+
229+
if !client.is_ready().await? {
230+
println!("runtime is not ready yet");
231+
}
232+
233+
for component in client.runtime_status().await? {
234+
println!("{} ({}): {}", component.name, component.endpoint, component.status);
235+
}
236+
// http (127.0.0.1:8090): Ready
237+
// flight (127.0.0.1:50051): Ready
238+
// metrics (N/A): Disabled
239+
// opentelemetry (127.0.0.1:50051): Ready
240+
241+
Ok(())
242+
}
243+
```
244+
245+
Each `ConnectionDetails` carries the component `name` (`http`, `flight`, `metrics` or
246+
`opentelemetry`), its `endpoint`, and its `status` — a `ComponentStatus` of `Initializing`,
247+
`Ready`, `Disabled`, `Error`, `Refreshing`, `ShuttingDown` or `NotLoaded`. A status a
248+
future runtime adds deserializes into `ComponentStatus::Other` rather than failing.
249+
213250
## Documentation
214251

215252
Check out our [Documentation](https://docs.spice.ai/sdks/rust-sdk) to learn more about how to use the Rust SDK.

src/client.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::{
77
config::{GenericError, SPICE_CLOUD_FLIGHT_ADDR, SPICE_LOCAL_FLIGHT_ADDR},
88
dataset::{DatasetError, DatasetRefreshRequest, DatasetRefreshResponse},
99
flight::{SqlFlightClient, is_connection_reset_generic_error},
10+
status::{ConnectionDetails, StatusError},
1011
tls::{FlightChannelBuilder, ensure_crypto_provider, new_tls_flight_channel},
1112
};
1213
use arrow::record_batch::RecordBatch;
@@ -677,6 +678,53 @@ impl SpiceClient {
677678

678679
http_client.refresh_dataset(dataset_name, &request).await
679680
}
681+
682+
/// Returns the status of each runtime connection.
683+
///
684+
/// Backed by `GET /v1/status`. Where [`is_ready`](Self::is_ready) reports a single
685+
/// boolean for the whole runtime, this reports `http`, `flight`, `metrics` and
686+
/// `opentelemetry` individually, so it can say *which* component is not ready.
687+
///
688+
/// **Note:** Requires [`http_url()`](SpiceClientBuilder::http_url) to be configured.
689+
///
690+
/// ```no_run
691+
/// # use spiceai::ClientBuilder;
692+
/// # #[tokio::main]
693+
/// # async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
694+
/// let client = ClientBuilder::new()
695+
/// .http_url("http://localhost:8090")
696+
/// .build()
697+
/// .await?;
698+
///
699+
/// for component in client.runtime_status().await? {
700+
/// println!("{} ({}): {}", component.name, component.endpoint, component.status);
701+
/// }
702+
/// # Ok(())
703+
/// # }
704+
/// ```
705+
pub async fn runtime_status(&self) -> Result<Vec<ConnectionDetails>, StatusError> {
706+
let http_client = self
707+
.http_client
708+
.as_ref()
709+
.ok_or(StatusError::HttpNotConfigured)?;
710+
711+
http_client.runtime_status().await
712+
}
713+
714+
/// Returns whether the runtime is ready to serve queries.
715+
///
716+
/// Backed by `GET /v1/ready`. Returns `Ok(false)` when the runtime responds that it
717+
/// is not ready; an `Err` means the probe itself could not be completed.
718+
///
719+
/// **Note:** Requires [`http_url()`](SpiceClientBuilder::http_url) to be configured.
720+
pub async fn is_ready(&self) -> Result<bool, StatusError> {
721+
let http_client = self
722+
.http_client
723+
.as_ref()
724+
.ok_or(StatusError::HttpNotConfigured)?;
725+
726+
http_client.is_ready().await
727+
}
680728
}
681729

682730
/// Builder for creating a `SpiceClient`.

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ mod flight;
88
mod params;
99
pub mod query;
1010
mod redirect;
11+
pub mod status;
1112
pub mod tls;
1213
mod util;
1314

@@ -24,6 +25,7 @@ pub use query::{
2425
QueryError, QueryInfo, QueryJob, QueryListResponse, QueryResult, QueryResultStream,
2526
QueryStatus, QuerySubmitOptions, QuerySummary,
2627
};
28+
pub use status::{ComponentStatus, ConnectionDetails, StatusError};
2729

2830
// Further public exports and integrations
2931
pub use futures::StreamExt;

src/query.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,21 @@ impl QueryHttpClient {
482482
}
483483
}
484484

485+
/// The underlying reqwest client, for request builders constructed in sibling modules.
486+
pub(crate) fn client(&self) -> &reqwest::Client {
487+
&self.client
488+
}
489+
490+
/// The runtime's HTTP base URL, with any trailing slash already trimmed.
491+
pub(crate) fn base_url(&self) -> &str {
492+
&self.base_url
493+
}
494+
495+
/// Applies the configured API key to a request, if one is set.
496+
pub(crate) fn authorized(&self, req: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
497+
self.add_auth(req)
498+
}
499+
485500
fn add_auth(&self, req: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
486501
match &self.api_key {
487502
Some(key) => req.header("X-API-Key", key),

0 commit comments

Comments
 (0)