hotshot_query_service/
status.rs1use std::{borrow::Cow, path::PathBuf};
27
28use futures::FutureExt;
29use tide_disco::{Api, api::ApiError, method::ReadState};
30use vbs::version::StaticVersionType;
31
32use crate::api::load_api;
33
34pub(crate) mod data_source;
35
36pub use data_source::*;
37pub use hotshot_query_service_types::status::Error;
38
39#[derive(Default)]
40pub struct Options {
41 pub api_path: Option<PathBuf>,
42
43 pub extensions: Vec<toml::Value>,
48}
49
50pub fn define_api<State, Ver: StaticVersionType + 'static>(
51 options: &Options,
52 _: Ver,
53 api_ver: semver::Version,
54) -> Result<Api<State, Error, Ver>, ApiError>
55where
56 State: 'static + Send + Sync + ReadState,
57 <State as ReadState>::State: Send + Sync + StatusDataSource,
58{
59 let mut api = load_api::<State, Error, Ver>(
60 options.api_path.as_ref(),
61 include_str!("../api/status.toml"),
62 options.extensions.clone(),
63 )?;
64 api.with_version(api_ver)
65 .get("block_height", |_, state| {
66 async { state.block_height().await.map_err(Error::internal) }.boxed()
67 })?
68 .get("success_rate", |_, state| {
69 async { state.success_rate().await.map_err(Error::internal) }.boxed()
70 })?
71 .get("time_since_last_decide", |_, state| {
72 async {
73 state
74 .elapsed_time_since_last_decide()
75 .await
76 .map_err(Error::internal)
77 }
78 .boxed()
79 })?
80 .metrics("metrics", |_, state| {
81 async { Ok(Cow::Borrowed(state.metrics())) }.boxed()
82 })?;
83 Ok(api)
84}
85
86#[cfg(test)]
87mod test {
88 use std::{str::FromStr, time::Duration};
89
90 use async_lock::RwLock;
91 use futures::FutureExt;
92 use reqwest::redirect::Policy;
93 use surf_disco::Client;
94 use tempfile::TempDir;
95 use test_utils::reserve_tcp_port;
96 use tide_disco::{App, StatusCode, Url};
97 use toml::toml;
98
99 use super::*;
100 use crate::{
101 ApiState, Error,
102 data_source::ExtensibleDataSource,
103 task::BackgroundTask,
104 testing::{
105 consensus::{MockDataSource, MockNetwork},
106 mocks::MockBase,
107 sleep,
108 },
109 };
110
111 #[test_log::test(tokio::test(flavor = "multi_thread"))]
112 async fn test_api() {
113 let mut network = MockNetwork::<MockDataSource>::init().await;
115
116 let port = reserve_tcp_port().unwrap();
118 let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source()));
119 app.register_module(
120 "status",
121 define_api(
122 &Default::default(),
123 MockBase::instance(),
124 "0.0.1".parse().unwrap(),
125 )
126 .unwrap(),
127 )
128 .unwrap();
129 network.spawn(
130 "server",
131 app.serve(format!("0.0.0.0:{port}"), MockBase::instance()),
132 );
133
134 let url = Url::from_str(&format!("http://localhost:{port}/status")).unwrap();
136 let client = Client::<Error, MockBase>::new(url.clone());
137 assert!(client.connect(Some(Duration::from_secs(60))).await);
138
139 assert_eq!(client.get::<u64>("block-height").send().await.unwrap(), 0);
141
142 let reqwest_client = reqwest::Client::builder()
145 .redirect(Policy::limited(5))
146 .build()
147 .unwrap();
148
149 let res = reqwest_client
151 .get(format!("{url}/metrics"))
152 .send()
153 .await
154 .unwrap();
155
156 assert_eq!(res.status(), StatusCode::OK);
158 let prometheus = res.text().await.unwrap();
159 let lines = prometheus.lines().collect::<Vec<_>>();
160 assert!(
161 lines.contains(&"consensus_current_view 0"),
162 "Missing consensus_current_view in metrics:\n{prometheus}"
163 );
164
165 network.start().await;
167
168 while client.get::<u64>("block-height").send().await.unwrap() <= 1 {
172 tracing::info!("waiting for block height to update");
173 sleep(Duration::from_secs(1)).await;
174 }
175 let success_rate = client.get::<f64>("success-rate").send().await.unwrap();
176 assert!(success_rate.is_finite(), "{success_rate}");
179 assert!(success_rate > 0.0, "{success_rate}");
181
182 network.shut_down().await;
183 }
184
185 #[test_log::test(tokio::test(flavor = "multi_thread"))]
186 async fn test_extensions() {
187 let dir = TempDir::with_prefix("test_status_extensions").unwrap();
188 let data_source = ExtensibleDataSource::new(
189 MockDataSource::create(dir.path(), Default::default())
190 .await
191 .unwrap(),
192 0,
193 );
194
195 let extensions = toml! {
196 [route.post_ext]
197 PATH = ["/ext/:val"]
198 METHOD = "POST"
199 ":val" = "Integer"
200
201 [route.get_ext]
202 PATH = ["/ext"]
203 METHOD = "GET"
204 };
205
206 let mut api = define_api::<RwLock<ExtensibleDataSource<MockDataSource, u64>>, MockBase>(
207 &Options {
208 extensions: vec![extensions.into()],
209 ..Default::default()
210 },
211 MockBase::instance(),
212 "0.0.1".parse().unwrap(),
213 )
214 .unwrap();
215 api.get("get_ext", |_, state| {
216 async move { Ok(*state.as_ref()) }.boxed()
217 })
218 .unwrap()
219 .post("post_ext", |req, state| {
220 async move {
221 *state.as_mut() = req.integer_param("val")?;
222 Ok(())
223 }
224 .boxed()
225 })
226 .unwrap();
227
228 let mut app = App::<_, Error>::with_state(RwLock::new(data_source));
229 app.register_module("status", api).unwrap();
230
231 let port = reserve_tcp_port().unwrap();
232 let _server = BackgroundTask::spawn(
233 "server",
234 app.serve(format!("0.0.0.0:{port}"), MockBase::instance()),
235 );
236
237 let client = Client::<Error, MockBase>::new(
238 format!("http://localhost:{port}/status").parse().unwrap(),
239 );
240 assert!(client.connect(Some(Duration::from_secs(60))).await);
241
242 assert_eq!(client.get::<u64>("ext").send().await.unwrap(), 0);
243 client.post::<()>("ext/42").send().await.unwrap();
244 assert_eq!(client.get::<u64>("ext").send().await.unwrap(), 42);
245
246 assert_eq!(client.get::<u64>("block-height").send().await.unwrap(), 0);
248 }
249}