use super::*;
use crate::worker::ControlPlaneWorker;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RetryPolicy {
pub max_attempts: usize,
pub initial_backoff_ms: u64,
pub max_backoff_ms: u64,
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_attempts: 3,
initial_backoff_ms: 50,
max_backoff_ms: 500,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ControlPlaneHttpConfig {
pub base_url: String,
pub timeout_ms: u64,
pub retry_policy: RetryPolicy,
}
#[derive(Clone, PartialEq, Eq)]
pub struct HttpControlPlaneRequest {
pub method: String,
pub path: String,
pub body: Vec<u8>,
pub timeout_ms: u64,
}
impl std::fmt::Debug for HttpControlPlaneRequest {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("HttpControlPlaneRequest")
.field("method", &self.method)
.field("path", &self.path)
.field("body_bytes", &self.body.len())
.field("timeout_ms", &self.timeout_ms)
.finish()
}
}
impl HttpControlPlaneRequest {
pub fn into_shared(self) -> SharedHttpControlPlaneRequest {
SharedHttpControlPlaneRequest {
method: self.method,
path: self.path,
body: Arc::from(self.body),
timeout_ms: self.timeout_ms,
}
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct SharedHttpControlPlaneRequest {
method: String,
path: String,
body: Arc<[u8]>,
timeout_ms: u64,
}
impl std::fmt::Debug for SharedHttpControlPlaneRequest {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("SharedHttpControlPlaneRequest")
.field("method", &self.method)
.field("path", &self.path)
.field("body_bytes", &self.body.len())
.field("timeout_ms", &self.timeout_ms)
.finish()
}
}
impl SharedHttpControlPlaneRequest {
pub fn method(&self) -> &str {
&self.method
}
pub fn path(&self) -> &str {
&self.path
}
pub fn body(&self) -> &[u8] {
&self.body
}
pub fn shared_body(&self) -> &Arc<[u8]> {
&self.body
}
pub fn timeout_ms(&self) -> u64 {
self.timeout_ms
}
fn to_owned(&self) -> HttpControlPlaneRequest {
HttpControlPlaneRequest {
method: self.method.clone(),
path: self.path.clone(),
body: self.body.to_vec(),
timeout_ms: self.timeout_ms,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HttpControlPlaneResponse {
pub status_code: u16,
pub body: Vec<u8>,
}
pub trait HttpTransport: Send + Sync {
fn send_json(
&self,
base_url: &str,
request: HttpControlPlaneRequest,
) -> ControlPlaneResult<HttpControlPlaneResponse>;
fn send_json_traced(
&self,
base_url: &str,
request: HttpControlPlaneRequest,
_trace: Option<&TraceContext>,
) -> ControlPlaneResult<HttpControlPlaneResponse> {
self.send_json(base_url, request)
}
fn send_json_traced_cancellable(
&self,
base_url: &str,
request: HttpControlPlaneRequest,
trace: Option<&TraceContext>,
cancellation: &CancellationToken,
) -> ControlPlaneResult<HttpControlPlaneResponse> {
if cancellation.is_cancelled() {
return Err(ControlPlaneError::Transport(
"control-plane request cancelled".to_string(),
));
}
self.send_json_traced(base_url, request, trace)
}
fn send_json_shared_traced_cancellable(
&self,
base_url: &str,
request: &SharedHttpControlPlaneRequest,
trace: Option<&TraceContext>,
cancellation: &CancellationToken,
) -> ControlPlaneResult<HttpControlPlaneResponse> {
self.send_json_traced_cancellable(base_url, request.to_owned(), trace, cancellation)
}
}
#[derive(Debug)]
pub struct HttpControlPlaneClient<T> {
config: ControlPlaneHttpConfig,
transport: Arc<T>,
worker: ControlPlaneWorker,
cancellation: CancellationToken,
request_started: std::time::Instant,
}
impl<T> Clone for HttpControlPlaneClient<T> {
fn clone(&self) -> Self {
Self {
config: self.config.clone(),
transport: Arc::clone(&self.transport),
worker: self.worker.clone(),
cancellation: self.cancellation.clone(),
request_started: self.request_started,
}
}
}
impl<T> HttpControlPlaneClient<T>
where
T: HttpTransport,
{
pub fn new(config: ControlPlaneHttpConfig, transport: T) -> Self {
Self {
config,
transport: Arc::new(transport),
worker: ControlPlaneWorker::new(),
cancellation: CancellationToken::new(),
request_started: std::time::Instant::now(),
}
}
pub fn with_cancellation_token(mut self, cancellation: CancellationToken) -> Self {
self.cancellation = cancellation;
self
}
pub fn cancel(&self) {
self.cancellation.cancel();
}
pub fn is_cancelled(&self) -> bool {
self.cancellation.is_cancelled()
}
fn post<Req, Resp>(&self, path: &str, value: &Req) -> ControlPlaneResult<Resp>
where
Req: Serialize,
Resp: for<'de> Deserialize<'de>,
{
self.post_traced(path, value, None)
}
fn for_request(&self) -> Self {
let started = std::time::Instant::now();
Self {
request_started: started,
..self.clone()
}
}
fn post_traced<Req, Resp>(
&self,
path: &str,
value: &Req,
trace: Option<&TraceContext>,
) -> ControlPlaneResult<Resp>
where
Req: Serialize,
Resp: for<'de> Deserialize<'de>,
{
let budget = crate::retry_budget::RetryBudget::new_at(&self.config, self.request_started)?;
budget.remaining()?;
let body = serde_json::to_vec(value)
.map_err(|error| ControlPlaneError::Transport(error.to_string()))?;
let request = HttpControlPlaneRequest {
method: "POST".to_string(),
path: path.to_string(),
body,
timeout_ms: self.config.timeout_ms,
};
self.send_with_retry::<Resp>(request, trace, budget)
}
fn send_with_retry<Resp>(
&self,
request: HttpControlPlaneRequest,
trace: Option<&TraceContext>,
mut budget: crate::retry_budget::RetryBudget,
) -> ControlPlaneResult<Resp>
where
Resp: for<'de> Deserialize<'de>,
{
let mut request = request.into_shared();
let attempts = if matches!(
request.path(),
CONTROL_SERVICE_LEASE_PATH | CONTROL_SERVICE_LEASE_RELEASE_PATH
) {
1 } else {
self.config.retry_policy.max_attempts.max(1)
};
let mut backoff_ms = self
.config
.retry_policy
.initial_backoff_ms
.min(self.config.retry_policy.max_backoff_ms);
let mut last_error = ControlPlaneError::Offline;
for attempt in 0..attempts {
request.timeout_ms = budget.attempt_timeout_ms(self.config.timeout_ms)?;
let response = self.transport.send_json_shared_traced_cancellable(
&self.config.base_url,
&request,
trace,
&self.cancellation,
);
budget.remaining()?;
match response {
Ok(response) if (200..300).contains(&response.status_code) => {
let result = serde_json::from_slice(&response.body)
.map_err(|error| ControlPlaneError::InvalidResponse(error.to_string()));
budget.remaining()?;
return result;
}
Ok(response) => {
last_error = ControlPlaneError::Rejected(format!(
"http_status={}",
response.status_code
));
if !matches!(response.status_code, 408 | 429 | 500 | 502 | 503 | 504) {
return Err(last_error);
}
}
Err(
error @ (ControlPlaneError::Timeout
| ControlPlaneError::Transport(_)
| ControlPlaneError::Offline),
) => last_error = error,
Err(error) => return Err(error),
}
if attempt + 1 < attempts {
if self
.cancellation
.wait_timeout(budget.retry_delay(backoff_ms)?)
{
return Err(ControlPlaneError::Transport(
"control-plane request cancelled".to_string(),
));
}
backoff_ms =
(backoff_ms.saturating_mul(2)).min(self.config.retry_policy.max_backoff_ms);
}
}
Err(last_error)
}
}
#[path = "client_provider.rs"]
mod provider;