diff --git a/apps/hermes/server/Cargo.lock b/apps/hermes/server/Cargo.lock index b853a87f6a..358f91f3f3 100644 --- a/apps/hermes/server/Cargo.lock +++ b/apps/hermes/server/Cargo.lock @@ -1868,7 +1868,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermes" -version = "0.8.5" +version = "0.8.6" dependencies = [ "anyhow", "async-trait", diff --git a/apps/hermes/server/Cargo.toml b/apps/hermes/server/Cargo.toml index 7c69e2a323..5c8f3921c9 100644 --- a/apps/hermes/server/Cargo.toml +++ b/apps/hermes/server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hermes" -version = "0.8.5" +version = "0.8.6" description = "Hermes is an agent that provides Verified Prices from the Pythnet Pyth Oracle." edition = "2021" diff --git a/apps/hermes/server/src/api/rest/ready.rs b/apps/hermes/server/src/api/rest/ready.rs index 92b538440d..b544a2a883 100644 --- a/apps/hermes/server/src/api/rest/ready.rs +++ b/apps/hermes/server/src/api/rest/ready.rs @@ -1,20 +1,42 @@ use { - crate::{api::ApiState, state::aggregate::Aggregates}, + crate::{ + api::ApiState, + state::{aggregate::Aggregates, cache::Cache}, + }, axum::{ extract::State, http::StatusCode, response::{IntoResponse, Response}, Json, }, + serde_json::json, }; +/// Endpoint that returns OK (200) only when the cache is fully hydrated. +/// +/// The cache is considered fully hydrated when all of the following conditions are met: +/// - `has_completed_recently`: The latest completed update is recent (within the staleness threshold) +/// - `is_not_behind`: The latest completed slot isn't too far behind the latest observed slot +/// - `is_metadata_loaded`: Price feeds metadata is not empty +/// +/// If any of these conditions are not met, the endpoint returns SERVICE_UNAVAILABLE (503) +/// along with detailed metadata about the readiness state. pub async fn ready(State(state): State>) -> Response where - S: Aggregates, + S: Aggregates + Cache, { let state = &*state.state; - match Aggregates::is_ready(state).await { - (true, _) => (StatusCode::OK, "OK").into_response(), - (false, metadata) => (StatusCode::SERVICE_UNAVAILABLE, Json(metadata)).into_response(), + let (aggregates_ready, metadata) = Aggregates::is_ready(state).await; + let cache_ready = Cache::is_cache_ready(state).await; + + if aggregates_ready && cache_ready { + (StatusCode::OK, "OK").into_response() + } else { + let response_metadata = json!({ + "aggregates_ready": aggregates_ready, + "cache_ready": cache_ready, + "details": metadata + }); + (StatusCode::SERVICE_UNAVAILABLE, Json(response_metadata)).into_response() } } diff --git a/apps/hermes/server/src/state/cache.rs b/apps/hermes/server/src/state/cache.rs index 8a0c1162af..1dffe7970f 100644 --- a/apps/hermes/server/src/state/cache.rs +++ b/apps/hermes/server/src/state/cache.rs @@ -136,6 +136,7 @@ pub trait Cache { request_time: RequestTime, filter: MessageStateFilter, ) -> Result>; + async fn is_cache_ready(&self) -> bool; } #[async_trait::async_trait] @@ -274,6 +275,20 @@ where let cache = self.into().wormhole_merkle_state_cache.read().await; Ok(cache.get(&slot).cloned()) } + + /// Checks if the cache contains sufficient data to serve requests. + /// Specifically, TWAP requests -- to serve a TWAP of X seconds, there + /// needs to be X seconds of data in the cache. We need to serve TWAPs + /// from the cache since TwapMessages only exist in Hermes at the moment, + /// not Benchmarks. + /// + /// The cache is considered ready when it is at or near its maximum slot capacity. + async fn is_cache_ready(&self) -> bool { + let message_cache = self.into().accumulator_messages_cache.read().await; + let max_cache_size = self.into().cache_size as usize; + + message_cache.len() >= (max_cache_size * 9 / 10) + } } async fn retrieve_message_state(