From c1c2795ae49e653b02af58e209e30919306d0490 Mon Sep 17 00:00:00 2001 From: eloylp Date: Tue, 2 Apr 2024 23:43:17 -0500 Subject: [PATCH] Add initial healthcheck endpoint (ampd) --- Cargo.lock | 215 ++++++++++++++++++++++------ ampd/Cargo.toml | 1 + ampd/README.md | 1 + ampd/src/config.rs | 4 + ampd/src/health_check.rs | 99 +++++++++++++ ampd/src/lib.rs | 18 +++ ampd/src/tests/config_template.toml | 1 + 7 files changed, 297 insertions(+), 42 deletions(-) create mode 100644 ampd/src/health_check.rs diff --git a/Cargo.lock b/Cargo.lock index 6bd497e37..41ee36401 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -155,6 +155,7 @@ version = "0.1.0" dependencies = [ "async-trait", "axelar-wasm-std", + "axum 0.7.5", "base64 0.21.4", "bcs", "clap", @@ -240,7 +241,7 @@ dependencies = [ "ed25519 1.5.3", "futures", "hex", - "http", + "http 0.2.9", "matchit 0.5.0", "pin-project-lite", "pkcs8 0.9.0", @@ -703,15 +704,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" dependencies = [ "async-trait", - "axum-core", + "axum-core 0.3.4", "base64 0.21.4", "bitflags 1.3.2", "bytes", "futures-util", "headers", - "http", - "http-body", - "hyper", + "http 0.2.9", + "http-body 0.4.5", + "hyper 0.14.27", "itoa", "matchit 0.7.2", "memchr", @@ -724,7 +725,7 @@ dependencies = [ "serde_path_to_error", "serde_urlencoded", "sha1", - "sync_wrapper", + "sync_wrapper 0.1.2", "tokio", "tokio-tungstenite", "tower", @@ -732,6 +733,40 @@ dependencies = [ "tower-service", ] +[[package]] +name = "axum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +dependencies = [ + "async-trait", + "axum-core 0.4.3", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.2.0", + "hyper-util", + "itoa", + "matchit 0.7.2", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.0", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "axum-core" version = "0.3.4" @@ -741,14 +776,35 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http", - "http-body", + "http 0.2.9", + "http-body 0.4.5", "mime", "rustversion", "tower-layer", "tower-service", ] +[[package]] +name = "axum-core" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 0.1.2", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "backtrace" version = "0.3.69" @@ -2803,7 +2859,7 @@ dependencies = [ "futures-timer", "futures-util", "hashers", - "http", + "http 0.2.9", "instant", "jsonwebtoken", "once_cell", @@ -3451,7 +3507,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.9", "indexmap 2.0.0", "slab", "tokio", @@ -3515,7 +3571,7 @@ dependencies = [ "base64 0.21.4", "bytes", "headers-core", - "http", + "http 0.2.9", "httpdate", "mime", "sha1", @@ -3527,7 +3583,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" dependencies = [ - "http", + "http 0.2.9", ] [[package]] @@ -3598,6 +3654,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.4.5" @@ -3605,7 +3672,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes", - "http", + "http 0.2.9", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +dependencies = [ + "bytes", + "futures-core", + "http 1.1.0", + "http-body 1.0.0", "pin-project-lite", ] @@ -3654,8 +3744,8 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", - "http-body", + "http 0.2.9", + "http-body 0.4.5", "httparse", "httpdate", "itoa", @@ -3667,6 +3757,25 @@ dependencies = [ "want", ] +[[package]] +name = "hyper" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + [[package]] name = "hyper-proxy" version = "0.9.1" @@ -3676,8 +3785,8 @@ dependencies = [ "bytes", "futures", "headers", - "http", - "hyper", + "http 0.2.9", + "hyper 0.14.27", "hyper-rustls 0.22.1", "rustls-native-certs", "tokio", @@ -3694,7 +3803,7 @@ checksum = "5f9f7a97316d44c0af9b0301e65010573a853a9fc97046d7331d7f6bc0fd5a64" dependencies = [ "ct-logs", "futures-util", - "hyper", + "hyper 0.14.27", "log", "rustls 0.19.1", "rustls-native-certs", @@ -3711,8 +3820,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" dependencies = [ "futures-util", - "http", - "hyper", + "http 0.2.9", + "hyper 0.14.27", "rustls 0.21.7", "tokio", "tokio-rustls 0.24.1", @@ -3724,12 +3833,28 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ - "hyper", + "hyper 0.14.27", "pin-project-lite", "tokio", "tokio-io-timeout", ] +[[package]] +name = "hyper-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "hyper 1.2.0", + "pin-project-lite", + "socket2 0.5.4", + "tokio", +] + [[package]] name = "iana-time-zone" version = "0.1.57" @@ -4889,7 +5014,7 @@ version = "0.7.0" source = "git+https://github.com/mystenlabs/sui?tag=mainnet-v1.14.2#299cbeafbb6aa5601e08f00ac24bd647c61a63e2" dependencies = [ "async-trait", - "axum", + "axum 0.6.20", "dashmap", "futures", "once_cell", @@ -4914,7 +5039,7 @@ dependencies = [ "bytes", "eyre", "futures", - "http", + "http 0.2.9", "multiaddr", "serde", "snap", @@ -6431,9 +6556,9 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", - "http-body", - "hyper", + "http 0.2.9", + "http-body 0.4.5", + "hyper 0.14.27", "hyper-rustls 0.24.1", "ipnet", "js-sys", @@ -6447,7 +6572,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 0.1.2", "system-configuration", "tokio", "tokio-rustls 0.24.1", @@ -7325,9 +7450,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "snap" @@ -7768,6 +7893,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "sync_wrapper" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384595c11a4e2969895cad5a8c4029115f5ab956a9e5ef4de79d11a426e5f20c" + [[package]] name = "synstructure" version = "0.12.6" @@ -7979,8 +8110,8 @@ dependencies = [ "flex-error", "futures", "getrandom", - "http", - "hyper", + "http 0.2.9", + "hyper 0.14.27", "hyper-proxy", "hyper-rustls 0.22.1", "peg", @@ -8321,15 +8452,15 @@ checksum = "8f219fad3b929bef19b1f86fbc0358d35daed8f2cac972037ac0dc10bbb8d5fb" dependencies = [ "async-stream", "async-trait", - "axum", + "axum 0.6.20", "base64 0.13.1", "bytes", "futures-core", "futures-util", "h2", - "http", - "http-body", - "hyper", + "http 0.2.9", + "http-body 0.4.5", + "hyper 0.14.27", "hyper-timeout", "percent-encoding", "pin-project", @@ -8353,13 +8484,13 @@ checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e" dependencies = [ "async-stream", "async-trait", - "axum", + "axum 0.6.20", "base64 0.21.4", "bytes", "h2", - "http", - "http-body", - "hyper", + "http 0.2.9", + "http-body 0.4.5", + "hyper 0.14.27", "hyper-timeout", "percent-encoding", "pin-project", @@ -8434,8 +8565,8 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http", - "http-body", + "http 0.2.9", + "http-body 0.4.5", "http-range-header", "httpdate", "iri-string", @@ -8582,7 +8713,7 @@ dependencies = [ "byteorder", "bytes", "data-encoding", - "http", + "http 0.2.9", "httparse", "log", "rand", diff --git a/ampd/Cargo.toml b/ampd/Cargo.toml index e259dd172..6a669234d 100644 --- a/ampd/Cargo.toml +++ b/ampd/Cargo.toml @@ -8,6 +8,7 @@ rust-version = { workspace = true } [dependencies] async-trait = "0.1.59" axelar-wasm-std = { workspace = true } +axum = "0.7.5" base64 = "0.21.2" bcs = "0.1.5" clap = { version = "4.2.7", features = ["derive", "cargo"] } diff --git a/ampd/README.md b/ampd/README.md index 87861cb68..734d68234 100644 --- a/ampd/README.md +++ b/ampd/README.md @@ -52,6 +52,7 @@ type=[handler type. Could be EvmMsgWorkerSetVerifier | SuiWorkerSetVerifier] Below is an example config for connecting to a local axelard node and local tofnd process, and verifying transactions from Avalanche testnet and Sui testnet. ``` +health_check_bind_addr = "0.0.0.0:3000" tm_jsonrpc="http://localhost:26657" tm_grpc="tcp://localhost:9090" event_buffer_cap=10000 diff --git a/ampd/src/config.rs b/ampd/src/config.rs index b9b625562..e65540f17 100644 --- a/ampd/src/config.rs +++ b/ampd/src/config.rs @@ -1,3 +1,5 @@ +use std::net::SocketAddr; +use std::str::FromStr; use std::time::Duration; use serde::{Deserialize, Serialize}; @@ -11,6 +13,7 @@ use crate::url::Url; #[derive(Deserialize, Serialize, Debug, PartialEq)] #[serde(default)] pub struct Config { + pub health_check_bind_addr: SocketAddr, pub tm_jsonrpc: Url, pub tm_grpc: Url, pub event_buffer_cap: usize, @@ -34,6 +37,7 @@ impl Default for Config { event_buffer_cap: 100000, event_stream_timeout: Duration::from_secs(15), service_registry: ServiceRegistryConfig::default(), + health_check_bind_addr: SocketAddr::from_str("0.0.0.0:3000").unwrap(), } } } diff --git a/ampd/src/health_check.rs b/ampd/src/health_check.rs new file mode 100644 index 000000000..030dddbf6 --- /dev/null +++ b/ampd/src/health_check.rs @@ -0,0 +1,99 @@ +use error_stack::{Result, ResultExt}; +use std::net::SocketAddr; +use thiserror::Error; + +use axum::{http::StatusCode, routing::get, Json, Router}; +use serde::{Deserialize, Serialize}; +use tokio_util::sync::CancellationToken; + +pub struct Server { + listener: tokio::net::TcpListener, +} + +#[derive(Error, Debug)] +pub enum HealthCheckError { + #[error("Health check server error: {0}")] + Error(String), +} + +impl Server { + pub async fn new(bind_addr: SocketAddr) -> Result { + Ok(Self { + listener: tokio::net::TcpListener::bind(bind_addr) + .await + .change_context(HealthCheckError::Error(format!( + "Failed binding to addr: {}", + bind_addr + )))?, + }) + } + + #[allow(dead_code)] + pub fn listening_addr(&self) -> Result { + Ok(self + .listener + .local_addr() + .map_err(|e| HealthCheckError::Error(e.to_string()))?) + } + + pub async fn run(self, cancel: CancellationToken) -> Result<(), HealthCheckError> { + tracing_subscriber::fmt::init(); + let app = Router::new().route("/status", get(status)); + axum::serve(self.listener, app) + .with_graceful_shutdown(async move { cancel.cancelled().await }) + .await + .change_context(HealthCheckError::Error( + "Failed executing server".to_string(), + )) + } +} + +// basic handler that responds with a static string +async fn status() -> (StatusCode, Json) { + (StatusCode::OK, Json(Status { ok: true })) +} + +#[derive(Serialize, Deserialize)] +struct Status { + ok: bool, +} + +#[cfg(test)] +mod tests { + + use std::{str::FromStr, time::Duration}; + + use super::*; + use tokio::test as async_test; + + #[async_test] + async fn server_lifecycle() { + let server = Server::new(SocketAddr::from_str("127.0.0.1:0").unwrap()) + .await + .unwrap(); + let listening_addr = server.listening_addr().unwrap(); + + let cancel = CancellationToken::new(); + + tokio::spawn(server.run(cancel.clone())); + + let url = format!("http://{}/status", listening_addr); + + tokio::time::sleep(Duration::from_millis(100)).await; + + let response = reqwest::get(&url).await.unwrap(); + assert_eq!(reqwest::StatusCode::OK, response.status()); + + let status = response.json::().await.unwrap(); + assert!(status.ok); + + cancel.cancel(); + + tokio::time::sleep(Duration::from_millis(100)).await; + + match reqwest::get(&url).await { + Ok(_) => panic!("health check server should be closed by now"), + Err(error) => assert!(error.is_connect()), + }; + } +} diff --git a/ampd/src/lib.rs b/ampd/src/lib.rs index ae4085418..0058e496c 100644 --- a/ampd/src/lib.rs +++ b/ampd/src/lib.rs @@ -35,6 +35,7 @@ mod event_processor; mod event_sub; mod evm; mod handlers; +mod health_check; mod json_rpc; mod queue; pub mod state; @@ -68,6 +69,7 @@ async fn prepare_app(cfg: Config, state: State) -> Result, event_buffer_cap, event_stream_timeout, service_registry: _service_registry, + health_check_bind_addr, } = cfg; let tm_client = tendermint_rpc::HttpClient::new(tm_jsonrpc.to_string().as_str()) @@ -119,6 +121,10 @@ async fn prepare_app(cfg: Config, state: State) -> Result, .build() .change_context(Error::Broadcaster)?; + let health_check_server = health_check::Server::new(health_check_bind_addr) + .await + .change_context(Error::HealthCheckServerError)?; + App::new( tm_client, broadcaster, @@ -127,6 +133,7 @@ async fn prepare_app(cfg: Config, state: State) -> Result, broadcast, event_buffer_cap, block_height_monitor, + health_check_server, ) .configure_handlers(worker, handlers, event_stream_timeout) } @@ -143,6 +150,7 @@ where state_updater: StateUpdater, ecdsa_client: SharableEcdsaClient, block_height_monitor: BlockHeightMonitor, + health_check_server: health_check::Server, token: CancellationToken, } @@ -158,6 +166,7 @@ where broadcast_cfg: broadcaster::Config, event_buffer_cap: usize, block_height_monitor: BlockHeightMonitor, + health_check_server: health_check::Server, ) -> Self { let token = CancellationToken::new(); @@ -183,6 +192,7 @@ where state_updater, ecdsa_client, block_height_monitor, + health_check_server, token, } } @@ -343,6 +353,7 @@ where broadcaster, state_updater, block_height_monitor, + health_check_server, token, .. } = self; @@ -375,6 +386,11 @@ where .run(token) .change_context(Error::EventPublisher) })) + .add_task(CancellableTask::create(|token| { + health_check_server + .run(token) + .change_context(Error::HealthCheckServerError) + })) .add_task(CancellableTask::create(|token| { event_processor .run(token) @@ -423,4 +439,6 @@ pub enum Error { InvalidInput, #[error("block height monitor failed")] BlockHeightMonitor, + #[error("Health check server error")] + HealthCheckServerError, } diff --git a/ampd/src/tests/config_template.toml b/ampd/src/tests/config_template.toml index 33ecfb8f0..952ec1f66 100644 --- a/ampd/src/tests/config_template.toml +++ b/ampd/src/tests/config_template.toml @@ -1,3 +1,4 @@ +health_check_bind_addr = '0.0.0.0:3000' tm_jsonrpc = 'http://localhost:26657/' tm_grpc = 'tcp://localhost:9090' event_buffer_cap = 100000