v0.2.1-pre.004
This commit is contained in:
@@ -1,26 +1,35 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/client.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
/// Passive runtime availability reported for one logical HTTP endpoint.
|
||||
/// Passive runtime availability reported for one logical HTTP endpoint or role.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum HttpEndpointAvailability {
|
||||
/// The endpoint is administratively disabled and cannot be selected.
|
||||
/// The endpoint or role is administratively disabled and cannot be selected.
|
||||
Disabled,
|
||||
/// The endpoint is enabled and currently eligible for selection.
|
||||
/// The endpoint or role is enabled and currently eligible for selection.
|
||||
Available,
|
||||
/// The endpoint is enabled but temporarily degraded by runtime observations.
|
||||
/// The endpoint or role is enabled but degraded by recent passive runtime observations.
|
||||
Degraded,
|
||||
/// The endpoint is enabled but temporarily excluded after provider rate limiting.
|
||||
/// The endpoint or role is temporarily excluded after provider rate limiting.
|
||||
RateLimited,
|
||||
}
|
||||
|
||||
/// Safe routing snapshot for one configured endpoint role.
|
||||
/// Safe routing and resilience snapshot for one configured endpoint role.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct HttpEndpointRoleSnapshot {
|
||||
role: std::string::String,
|
||||
enabled: bool,
|
||||
request_kinds: std::vec::Vec<std::string::String>,
|
||||
priority: u32,
|
||||
availability: crate::HttpEndpointAvailability,
|
||||
requests_per_second: std::option::Option<u32>,
|
||||
burst_capacity: std::option::Option<u32>,
|
||||
max_concurrent_requests: std::option::Option<u32>,
|
||||
in_flight_requests: std::option::Option<u32>,
|
||||
cooldown_remaining: std::option::Option<std::time::Duration>,
|
||||
success_count: u64,
|
||||
failure_count: u64,
|
||||
rate_limit_count: u64,
|
||||
}
|
||||
|
||||
impl HttpEndpointRoleSnapshot {
|
||||
@@ -47,6 +56,60 @@ impl HttpEndpointRoleSnapshot {
|
||||
pub const fn priority(&self) -> u32 {
|
||||
return self.priority;
|
||||
}
|
||||
|
||||
/// Returns the passive runtime availability of this role.
|
||||
#[must_use]
|
||||
pub const fn availability(&self) -> crate::HttpEndpointAvailability {
|
||||
return self.availability;
|
||||
}
|
||||
|
||||
/// Returns the configured requests-per-second limit.
|
||||
#[must_use]
|
||||
pub const fn requests_per_second(&self) -> std::option::Option<u32> {
|
||||
return self.requests_per_second;
|
||||
}
|
||||
|
||||
/// Returns the configured token-bucket burst capacity.
|
||||
#[must_use]
|
||||
pub const fn burst_capacity(&self) -> std::option::Option<u32> {
|
||||
return self.burst_capacity;
|
||||
}
|
||||
|
||||
/// Returns the configured maximum concurrent request count.
|
||||
#[must_use]
|
||||
pub const fn max_concurrent_requests(&self) -> std::option::Option<u32> {
|
||||
return self.max_concurrent_requests;
|
||||
}
|
||||
|
||||
/// Returns the number of in-flight requests when concurrency is bounded.
|
||||
#[must_use]
|
||||
pub const fn in_flight_requests(&self) -> std::option::Option<u32> {
|
||||
return self.in_flight_requests;
|
||||
}
|
||||
|
||||
/// Returns the remaining provider cooldown when this role is rate-limited.
|
||||
#[must_use]
|
||||
pub const fn cooldown_remaining(&self) -> std::option::Option<std::time::Duration> {
|
||||
return self.cooldown_remaining;
|
||||
}
|
||||
|
||||
/// Returns the number of successful requests passively recorded for this role.
|
||||
#[must_use]
|
||||
pub const fn success_count(&self) -> u64 {
|
||||
return self.success_count;
|
||||
}
|
||||
|
||||
/// Returns the number of failed requests passively recorded for this role.
|
||||
#[must_use]
|
||||
pub const fn failure_count(&self) -> u64 {
|
||||
return self.failure_count;
|
||||
}
|
||||
|
||||
/// Returns the number of provider rate-limit observations recorded for this role.
|
||||
#[must_use]
|
||||
pub const fn rate_limit_count(&self) -> u64 {
|
||||
return self.rate_limit_count;
|
||||
}
|
||||
}
|
||||
|
||||
/// Safe metadata snapshot for one logical HTTP endpoint.
|
||||
@@ -111,12 +174,16 @@ pub struct HttpEndpointClient {
|
||||
struct HttpEndpointClientInner {
|
||||
settings: crate::HttpEndpointSettings,
|
||||
_client: reqwest::Client,
|
||||
availability: std::sync::atomic::AtomicU8,
|
||||
role_runtimes: std::vec::Vec<std::sync::Arc<crate::resilience::HttpRoleRuntime>>,
|
||||
}
|
||||
|
||||
impl HttpEndpointClient {
|
||||
/// Builds one logical endpoint client from KSP-owned runtime settings.
|
||||
pub fn new(settings: crate::HttpEndpointSettings) -> ksp_core_lib::Result<Self> {
|
||||
return Self::new_with_notify(settings, std::sync::Arc::new(tokio::sync::Notify::new()));
|
||||
}
|
||||
|
||||
pub(crate) fn new_with_notify(settings: crate::HttpEndpointSettings, notify: std::sync::Arc<tokio::sync::Notify>) -> ksp_core_lib::Result<Self> {
|
||||
let validation = crate::settings::validate_endpoint_settings(&settings);
|
||||
if let std::result::Result::Err(error) = validation {
|
||||
return std::result::Result::Err(error);
|
||||
@@ -132,18 +199,20 @@ impl HttpEndpointClient {
|
||||
);
|
||||
},
|
||||
};
|
||||
let availability = if settings.enabled() { AVAILABILITY_AVAILABLE } else { AVAILABILITY_DISABLED };
|
||||
let mut role_runtimes = std::vec::Vec::with_capacity(settings.roles().len());
|
||||
for role in settings.roles() {
|
||||
role_runtimes.push(std::sync::Arc::new(crate::resilience::HttpRoleRuntime::new(role, std::sync::Arc::clone(¬ify))));
|
||||
}
|
||||
ksp_logging_lib::debug!(
|
||||
target: env!("CARGO_PKG_NAME"),
|
||||
endpoint_name = settings.name(),
|
||||
provider = settings.provider().as_str(),
|
||||
cluster = settings.cluster().as_str(),
|
||||
enabled = settings.enabled(),
|
||||
role_count = settings.roles().len(),
|
||||
"created logical HTTP endpoint client"
|
||||
);
|
||||
return std::result::Result::Ok(Self {
|
||||
inner: std::sync::Arc::new(HttpEndpointClientInner { settings, _client: client, availability: std::sync::atomic::AtomicU8::new(availability) }),
|
||||
});
|
||||
return std::result::Result::Ok(Self { inner: std::sync::Arc::new(HttpEndpointClientInner { settings, _client: client, role_runtimes }) });
|
||||
}
|
||||
|
||||
/// Returns the endpoint identity used for safe diagnostics and routing.
|
||||
@@ -170,7 +239,13 @@ impl HttpEndpointClient {
|
||||
return self.inner.settings.enabled();
|
||||
}
|
||||
|
||||
/// Returns whether one enabled role can serve the requested capability.
|
||||
/// Returns the configured end-to-end request timeout.
|
||||
#[must_use]
|
||||
pub fn request_timeout(&self) -> std::time::Duration {
|
||||
return self.inner.settings.request_timeout();
|
||||
}
|
||||
|
||||
/// Returns whether one enabled role can serve the requested capability structurally.
|
||||
#[must_use]
|
||||
pub fn supports(&self, role: &crate::HttpRoleName, request_kind: &crate::HttpRequestKind) -> bool {
|
||||
return self.matching_role(role, request_kind).is_some();
|
||||
@@ -180,17 +255,17 @@ impl HttpEndpointClient {
|
||||
#[must_use]
|
||||
pub fn snapshot(&self) -> crate::HttpEndpointSnapshot {
|
||||
let mut roles = std::vec::Vec::with_capacity(self.inner.settings.roles().len());
|
||||
for role in self.inner.settings.roles() {
|
||||
for (role_index, role) in self.inner.settings.roles().iter().enumerate() {
|
||||
let mut request_kinds = std::vec::Vec::with_capacity(role.request_kinds().len());
|
||||
for request_kind in role.request_kinds() {
|
||||
request_kinds.push(request_kind.as_str().to_owned());
|
||||
}
|
||||
roles.push(crate::HttpEndpointRoleSnapshot {
|
||||
role: role.role().as_str().to_owned(),
|
||||
enabled: role.enabled(),
|
||||
request_kinds,
|
||||
priority: role.priority(),
|
||||
});
|
||||
let runtime = self.inner.role_runtimes.get(role_index);
|
||||
let role_snapshot = match runtime {
|
||||
std::option::Option::Some(runtime) => role_snapshot(role, request_kinds, runtime),
|
||||
std::option::Option::None => fallback_role_snapshot(role, request_kinds),
|
||||
};
|
||||
roles.push(role_snapshot);
|
||||
}
|
||||
return crate::HttpEndpointSnapshot {
|
||||
name: self.name().to_owned(),
|
||||
@@ -207,7 +282,7 @@ impl HttpEndpointClient {
|
||||
role: &crate::HttpRoleName,
|
||||
request_kind: &crate::HttpRequestKind,
|
||||
) -> std::option::Option<&'a crate::HttpEndpointRoleSettings> {
|
||||
if !self.is_selectable() {
|
||||
if !self.enabled() {
|
||||
return std::option::Option::None;
|
||||
}
|
||||
for candidate_role in self.inner.settings.roles() {
|
||||
@@ -223,12 +298,68 @@ impl HttpEndpointClient {
|
||||
return std::option::Option::None;
|
||||
}
|
||||
|
||||
fn is_selectable(&self) -> bool {
|
||||
return self.enabled() && self.availability() == crate::HttpEndpointAvailability::Available;
|
||||
pub(crate) fn matching_role_runtime(
|
||||
&self,
|
||||
role: &crate::HttpRoleName,
|
||||
request_kind: &crate::HttpRequestKind,
|
||||
) -> std::option::Option<(u32, std::sync::Arc<crate::resilience::HttpRoleRuntime>)> {
|
||||
if !self.enabled() {
|
||||
return std::option::Option::None;
|
||||
}
|
||||
for (role_index, candidate_role) in self.inner.settings.roles().iter().enumerate() {
|
||||
if !candidate_role.enabled() || candidate_role.role() != role {
|
||||
continue;
|
||||
}
|
||||
let mut handles = false;
|
||||
for capability in candidate_role.request_kinds() {
|
||||
if capability.is_wildcard() || capability == request_kind {
|
||||
handles = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !handles {
|
||||
continue;
|
||||
}
|
||||
let runtime = self.inner.role_runtimes.get(role_index);
|
||||
if let std::option::Option::Some(runtime) = runtime {
|
||||
return std::option::Option::Some((candidate_role.priority(), std::sync::Arc::clone(runtime)));
|
||||
}
|
||||
}
|
||||
return std::option::Option::None;
|
||||
}
|
||||
|
||||
fn availability(&self) -> crate::HttpEndpointAvailability {
|
||||
return availability_from_code(self.inner.availability.load(std::sync::atomic::Ordering::Relaxed));
|
||||
pub(crate) fn availability(&self) -> crate::HttpEndpointAvailability {
|
||||
if !self.enabled() {
|
||||
return crate::HttpEndpointAvailability::Disabled;
|
||||
}
|
||||
let now = std::time::Instant::now();
|
||||
let mut enabled_role_count = 0_usize;
|
||||
let mut rate_limited_count = 0_usize;
|
||||
let mut degraded = false;
|
||||
for (role_index, role) in self.inner.settings.roles().iter().enumerate() {
|
||||
if !role.enabled() {
|
||||
continue;
|
||||
}
|
||||
enabled_role_count = enabled_role_count.saturating_add(1);
|
||||
let runtime = self.inner.role_runtimes.get(role_index);
|
||||
let availability = match runtime {
|
||||
std::option::Option::Some(runtime) => runtime.availability(now),
|
||||
std::option::Option::None => crate::HttpEndpointAvailability::Degraded,
|
||||
};
|
||||
if availability == crate::HttpEndpointAvailability::RateLimited {
|
||||
rate_limited_count = rate_limited_count.saturating_add(1);
|
||||
}
|
||||
if availability == crate::HttpEndpointAvailability::Degraded || availability == crate::HttpEndpointAvailability::RateLimited {
|
||||
degraded = true;
|
||||
}
|
||||
}
|
||||
if enabled_role_count > 0 && rate_limited_count == enabled_role_count {
|
||||
return crate::HttpEndpointAvailability::RateLimited;
|
||||
}
|
||||
if degraded || enabled_role_count == 0 {
|
||||
return crate::HttpEndpointAvailability::Degraded;
|
||||
}
|
||||
return crate::HttpEndpointAvailability::Available;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,6 +369,47 @@ impl std::fmt::Debug for HttpEndpointClient {
|
||||
}
|
||||
}
|
||||
|
||||
fn role_snapshot(
|
||||
role: &crate::HttpEndpointRoleSettings,
|
||||
request_kinds: std::vec::Vec<std::string::String>,
|
||||
runtime: &crate::resilience::HttpRoleRuntime,
|
||||
) -> crate::HttpEndpointRoleSnapshot {
|
||||
let availability = if role.enabled() { runtime.availability(std::time::Instant::now()) } else { crate::HttpEndpointAvailability::Disabled };
|
||||
return crate::HttpEndpointRoleSnapshot {
|
||||
role: role.role().as_str().to_owned(),
|
||||
enabled: role.enabled(),
|
||||
request_kinds,
|
||||
priority: role.priority(),
|
||||
availability,
|
||||
requests_per_second: role.limits().requests_per_second().map(|value| return value.get()),
|
||||
burst_capacity: role.limits().burst_capacity().map(|value| return value.get()),
|
||||
max_concurrent_requests: runtime.max_concurrent_requests(),
|
||||
in_flight_requests: runtime.in_flight_requests(),
|
||||
cooldown_remaining: runtime.cooldown_remaining(),
|
||||
success_count: runtime.success_count(),
|
||||
failure_count: runtime.failure_count(),
|
||||
rate_limit_count: runtime.rate_limit_count(),
|
||||
};
|
||||
}
|
||||
|
||||
fn fallback_role_snapshot(role: &crate::HttpEndpointRoleSettings, request_kinds: std::vec::Vec<std::string::String>) -> crate::HttpEndpointRoleSnapshot {
|
||||
return crate::HttpEndpointRoleSnapshot {
|
||||
role: role.role().as_str().to_owned(),
|
||||
enabled: role.enabled(),
|
||||
request_kinds,
|
||||
priority: role.priority(),
|
||||
availability: crate::HttpEndpointAvailability::Degraded,
|
||||
requests_per_second: role.limits().requests_per_second().map(|value| return value.get()),
|
||||
burst_capacity: role.limits().burst_capacity().map(|value| return value.get()),
|
||||
max_concurrent_requests: role.limits().max_concurrent_requests().map(|value| return value.get()),
|
||||
in_flight_requests: std::option::Option::None,
|
||||
cooldown_remaining: std::option::Option::None,
|
||||
success_count: 0,
|
||||
failure_count: 0,
|
||||
rate_limit_count: 0,
|
||||
};
|
||||
}
|
||||
|
||||
fn build_reqwest_client(settings: &crate::HttpEndpointSettings) -> std::result::Result<reqwest::Client, reqwest::Error> {
|
||||
let mut builder = reqwest::Client::builder()
|
||||
.connect_timeout(settings.connect_timeout())
|
||||
@@ -251,24 +423,6 @@ fn build_reqwest_client(settings: &crate::HttpEndpointSettings) -> std::result::
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
const AVAILABILITY_DISABLED: u8 = 0;
|
||||
const AVAILABILITY_AVAILABLE: u8 = 1;
|
||||
const AVAILABILITY_DEGRADED: u8 = 2;
|
||||
const AVAILABILITY_RATE_LIMITED: u8 = 3;
|
||||
|
||||
fn availability_from_code(code: u8) -> crate::HttpEndpointAvailability {
|
||||
if code == AVAILABILITY_DISABLED {
|
||||
return crate::HttpEndpointAvailability::Disabled;
|
||||
}
|
||||
if code == AVAILABILITY_DEGRADED {
|
||||
return crate::HttpEndpointAvailability::Degraded;
|
||||
}
|
||||
if code == AVAILABILITY_RATE_LIMITED {
|
||||
return crate::HttpEndpointAvailability::RateLimited;
|
||||
}
|
||||
return crate::HttpEndpointAvailability::Available;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/client.rs"]
|
||||
mod tests;
|
||||
|
||||
Reference in New Issue
Block a user