use std::io;
use std::time::Duration;
use openraft::RaftMetrics;
use serde::Serialize;
use serde::de::DeserializeOwned;
use tokio::time::sleep;
use crate::admin::membership_change::MembershipChange;
use crate::admin::redirect::Redirect;
use crate::app::EzApp;
use crate::type_config::OpenRaftTypes;
const ADMIN_ATTEMPTS: usize = 20;
const ADMIN_RETRY_INTERVAL: Duration = Duration::from_millis(500);
const ADMIN_TIMEOUT: Duration = Duration::from_secs(5);
const MEMBERSHIP_CHANGE_TIMEOUT: Duration = Duration::from_secs(90);
#[derive(Debug, Clone)]
pub struct AdminClient {
addr: String,
}
impl AdminClient {
pub fn new(addr: impl ToString) -> Self {
Self { addr: addr.to_string() }
}
pub async fn node_id(&self) -> Result<u64, io::Error> {
self.request("node_id", &(), ADMIN_TIMEOUT).await
}
pub async fn membership(&self, change: MembershipChange) -> Result<(), io::Error> {
let timeout = match change {
MembershipChange::SetRole { .. } => MEMBERSHIP_CHANGE_TIMEOUT,
_ => ADMIN_TIMEOUT,
};
self.request("membership", &change, timeout).await
}
pub async fn metrics<T>(&self) -> Result<RaftMetrics<OpenRaftTypes<T>>, io::Error>
where T: EzApp {
let url = format!("http://{}/api/metrics", self.addr);
answer(&url, client(ADMIN_TIMEOUT)?.get(&url)).await
}
async fn request<Req, Resp>(&self, endpoint: &str, req: &Req, timeout: Duration) -> Result<Resp, io::Error>
where
Req: Serialize,
Resp: DeserializeOwned,
{
let client = client(timeout)?;
let mut target_addr = self.addr.clone();
let mut last_err = format!("cluster did not answer {}", endpoint);
for _ in 0..ADMIN_ATTEMPTS {
let url = format!("http://{}/api/{}", target_addr, endpoint);
let redirect: Redirect<Resp> = match answer(&url, client.post(&url).json(req)).await {
Ok(redirect) => redirect,
Err(e) => {
last_err = e.to_string();
sleep(ADMIN_RETRY_INTERVAL).await;
continue;
}
};
match redirect {
Ok(resp) => return Ok(resp),
Err(Some(leader)) => {
last_err = format!("{} redirected to {}", target_addr, leader);
target_addr = leader;
}
Err(None) => {
last_err = format!("{} knows of no leader", target_addr);
sleep(ADMIN_RETRY_INTERVAL).await;
}
}
}
Err(io::Error::other(format!(
"{} gave up after {} attempts: {}",
endpoint, ADMIN_ATTEMPTS, last_err
)))
}
}
async fn answer<Resp>(url: &str, req: reqwest::RequestBuilder) -> Result<Resp, io::Error>
where Resp: DeserializeOwned {
let resp = req.send().await.map_err(|e| io::Error::other(format!("request to {} failed: {}", url, e)))?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(io::Error::other(format!("{} responded {}: {}", url, status, body)));
}
resp.json().await.map_err(|e| io::Error::other(format!("failed to parse {} response: {}", url, e)))
}
fn client(timeout: Duration) -> Result<reqwest::Client, io::Error> {
reqwest::Client::builder()
.no_proxy()
.timeout(timeout)
.build()
.map_err(|e| io::Error::other(e.to_string()))
}