Skip to content

Commit

Permalink
Merge pull request #2107 from input-output-hk/dlachaume/2071/create-s…
Browse files Browse the repository at this point in the history
…tatus-route

Feat: implement `/status` route to expose aggregator information
  • Loading branch information
dlachaume authored Nov 13, 2024
2 parents 913172d + 99b0c4d commit e2fa1e0
Show file tree
Hide file tree
Showing 13 changed files with 556 additions and 11 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ As a minor extension, we have adopted a slightly different versioning convention

- Implement a new `genesis generate-keypair` command in aggregator CLI to generate a new genesis keypair.

- Implement the `/status` route on the aggregator's REST API to provide information about its current status.

- Crates versions:

| Crate | Version |
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion mithril-aggregator/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "mithril-aggregator"
version = "0.5.109"
version = "0.5.110"
description = "A Mithril Aggregator server"
authors = { workspace = true }
edition = { workspace = true }
Expand Down
1 change: 1 addition & 0 deletions mithril-aggregator/src/dependency_injection/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1522,6 +1522,7 @@ impl DependenciesBuilder {
.cardano_transactions_signing_config
.clone(),
snapshot_directory: self.configuration.snapshot_directory.clone(),
cardano_node_version: self.configuration.cardano_node_version.clone(),
},
);

Expand Down
1 change: 1 addition & 0 deletions mithril-aggregator/src/http_server/routes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub mod router;
mod signatures_routes;
mod signer_routes;
mod statistics_routes;
mod status;

/// Match the given result and do an early return with an internal server error (500)
/// if it was an Error. Else return the unwrapped value.
Expand Down
5 changes: 4 additions & 1 deletion mithril-aggregator/src/http_server/routes/router.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::http_server::routes::{
artifact_routes, certificate_routes, epoch_routes, http_server_child_logger, root_routes,
signatures_routes, signer_routes, statistics_routes,
signatures_routes, signer_routes, statistics_routes, status,
};
use crate::http_server::SERVER_BASE_PATH;
use crate::DependencyContainer;
Expand Down Expand Up @@ -38,6 +38,7 @@ pub struct RouterConfig {
pub cardano_transactions_prover_max_hashes_allowed_by_request: usize,
pub cardano_transactions_signing_config: CardanoTransactionsSigningConfig,
pub snapshot_directory: PathBuf,
pub cardano_node_version: String,
}

#[cfg(test)]
Expand All @@ -53,6 +54,7 @@ impl RouterConfig {
cardano_transactions_prover_max_hashes_allowed_by_request: 1_000,
cardano_transactions_signing_config: CardanoTransactionsSigningConfig::dummy(),
snapshot_directory: PathBuf::from("/dummy/snapshot/directory"),
cardano_node_version: "1.2.3".to_string(),
}
}
}
Expand Down Expand Up @@ -110,6 +112,7 @@ pub fn routes(
.or(epoch_routes::routes(&state))
.or(statistics_routes::routes(&state))
.or(root_routes::routes(&state))
.or(status::routes(&state))
.with(cors),
)
.recover(handle_custom)
Expand Down
276 changes: 276 additions & 0 deletions mithril-aggregator/src/http_server/routes/status.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
use warp::Filter;

use mithril_common::{messages::AggregatorStatusMessage, StdResult};

use crate::{
dependency_injection::EpochServiceWrapper,
http_server::routes::{middlewares, router::RouterState},
};

pub fn routes(
router_state: &RouterState,
) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
status(router_state)
}

/// GET /status
fn status(
router_state: &RouterState,
) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
warp::path!("status")
.and(warp::get())
.and(middlewares::with_logger(router_state))
.and(middlewares::with_epoch_service(router_state))
.and(middlewares::extract_config(router_state, |config| {
config.cardano_node_version.clone()
}))
.and_then(handlers::status)
}

async fn get_aggregator_status_message(
epoch_service: EpochServiceWrapper,
cardano_node_version: String,
) -> StdResult<AggregatorStatusMessage> {
let epoch_service = epoch_service.read().await;

let epoch = epoch_service.epoch_of_current_data()?;
let cardano_era = epoch_service.cardano_era()?;
let mithril_era = epoch_service.mithril_era()?;
let aggregator_node_version = env!("CARGO_PKG_VERSION").to_string();
let protocol_parameters = epoch_service.current_protocol_parameters()?.clone();
let next_protocol_parameters = epoch_service.next_protocol_parameters()?.clone();
let total_signers = epoch_service.current_signers()?.len();
let total_next_signers = epoch_service.next_signers()?.len();
let total_stakes_signers = epoch_service.total_stakes_signers()?;
let total_next_stakes_signers = epoch_service.total_next_stakes_signers()?;
let total_cardano_spo = epoch_service.total_spo()?;
let total_cardano_stake = epoch_service.total_stake()?;

let message = AggregatorStatusMessage {
epoch,
cardano_era,
mithril_era,
cardano_node_version,
aggregator_node_version,
protocol_parameters,
next_protocol_parameters,
total_signers,
total_next_signers,
total_stakes_signers,
total_next_stakes_signers,
total_cardano_spo,
total_cardano_stake,
};

Ok(message)
}

mod handlers {
use std::convert::Infallible;

use slog::{warn, Logger};
use warp::http::StatusCode;

use crate::{
dependency_injection::EpochServiceWrapper,
http_server::routes::{reply, status::get_aggregator_status_message},
};

/// Status
pub async fn status(
logger: Logger,
epoch_service: EpochServiceWrapper,
cardano_node_version: String,
) -> Result<impl warp::Reply, Infallible> {
let aggregator_status_message =
get_aggregator_status_message(epoch_service, cardano_node_version).await;

match aggregator_status_message {
Ok(message) => Ok(reply::json(&message, StatusCode::OK)),
Err(err) => {
warn!(logger,"aggregator_status::error"; "error" => ?err);
Ok(reply::server_error(err))
}
}
}
}

#[cfg(test)]
mod tests {
use std::sync::Arc;

use serde_json::Value::Null;
use tokio::sync::RwLock;
use warp::{
http::{Method, StatusCode},
test::request,
};

use mithril_common::{
entities::{Epoch, ProtocolParameters, Stake},
test_utils::{apispec::APISpec, fake_data, MithrilFixtureBuilder},
};

use crate::{
entities::AggregatorEpochSettings,
http_server::SERVER_BASE_PATH,
initialize_dependencies,
services::{FakeEpochService, FakeEpochServiceBuilder},
};

use super::*;

fn setup_router(
state: RouterState,
) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
let cors = warp::cors()
.allow_any_origin()
.allow_headers(vec!["content-type"])
.allow_methods(vec![Method::GET, Method::POST, Method::OPTIONS]);

warp::any()
.and(warp::path(SERVER_BASE_PATH))
.and(routes(&state).with(cors))
}

#[tokio::test]
async fn status_route_ko_500() {
let dependency_manager = initialize_dependencies().await;
let method = Method::GET.as_str();
let path = "/status";

let response = request()
.method(method)
.path(&format!("/{SERVER_BASE_PATH}{path}"))
.reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
dependency_manager,
))))
.await;

APISpec::verify_conformity(
APISpec::get_all_spec_files(),
method,
path,
"application/json",
&Null,
&response,
&StatusCode::INTERNAL_SERVER_ERROR,
)
.unwrap();
}

#[tokio::test]
async fn status_route_ok_200() {
let mut dependency_manager = initialize_dependencies().await;
let fixture = MithrilFixtureBuilder::default().build();
let epoch_service = FakeEpochService::from_fixture(Epoch(5), &fixture);
dependency_manager.epoch_service = Arc::new(RwLock::new(epoch_service));

let method = Method::GET.as_str();
let path = "/status";

let response = request()
.method(method)
.path(&format!("/{SERVER_BASE_PATH}{path}"))
.reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
dependency_manager,
))))
.await;

APISpec::verify_conformity(
APISpec::get_all_spec_files(),
method,
path,
"application/json",
&Null,
&response,
&StatusCode::OK,
)
.unwrap();
}

#[tokio::test]
async fn retrieves_correct_protocol_parameters_from_epoch_service() {
let current_epoch_settings = AggregatorEpochSettings {
protocol_parameters: ProtocolParameters::new(101, 10, 0.5),
..AggregatorEpochSettings::dummy()
};
let next_epoch_settings = AggregatorEpochSettings {
protocol_parameters: ProtocolParameters::new(102, 20, 0.5),
..AggregatorEpochSettings::dummy()
};
let signer_registration_epoch_settings = AggregatorEpochSettings {
protocol_parameters: ProtocolParameters::new(103, 30, 0.5),
..AggregatorEpochSettings::dummy()
};

let epoch_service = FakeEpochServiceBuilder {
current_epoch_settings: current_epoch_settings.clone(),
next_epoch_settings: next_epoch_settings.clone(),
signer_registration_epoch_settings,
..FakeEpochServiceBuilder::dummy(Epoch(3))
}
.build();

let message =
get_aggregator_status_message(Arc::new(RwLock::new(epoch_service)), String::new())
.await
.unwrap();

assert_eq!(
message.protocol_parameters,
current_epoch_settings.protocol_parameters
);

assert_eq!(
message.next_protocol_parameters,
next_epoch_settings.protocol_parameters
);
}

#[tokio::test]
async fn retrieves_correct_total_signers_from_epoch_service() {
let total_signers = 12;
let total_next_signers = 345;
let epoch_service = FakeEpochServiceBuilder {
current_signers_with_stake: fake_data::signers_with_stakes(total_signers),
next_signers_with_stake: fake_data::signers_with_stakes(total_next_signers),
..FakeEpochServiceBuilder::dummy(Epoch(3))
}
.build();
let epoch_service = Arc::new(RwLock::new(epoch_service));

let message = get_aggregator_status_message(epoch_service.clone(), String::new())
.await
.unwrap();

assert_eq!(message.total_signers, total_signers);
assert_eq!(message.total_next_signers, total_next_signers);
}

#[tokio::test]
async fn retrieves_correct_total_stakes_from_epoch_service() {
let current_signers_with_stake = fake_data::signers_with_stakes(4);
let next_signers_with_stake = fake_data::signers_with_stakes(7);
let total_stakes_signers: Stake = current_signers_with_stake.iter().map(|s| s.stake).sum();
let total_next_stakes_signers: Stake =
next_signers_with_stake.iter().map(|s| s.stake).sum();

assert_ne!(total_stakes_signers, total_next_stakes_signers);

let epoch_service = FakeEpochServiceBuilder {
current_signers_with_stake,
next_signers_with_stake,
..FakeEpochServiceBuilder::dummy(Epoch(3))
}
.build();
let epoch_service = Arc::new(RwLock::new(epoch_service));

let message = get_aggregator_status_message(epoch_service.clone(), String::new())
.await
.unwrap();

assert_eq!(message.total_stakes_signers, total_stakes_signers);
assert_eq!(message.total_next_stakes_signers, total_next_stakes_signers);
}
}
Loading

0 comments on commit e2fa1e0

Please sign in to comment.