v0.2.1-pre.004

This commit is contained in:
2026-08-17 19:26:26 +02:00
parent ed978179d8
commit c1cea6e813
15 changed files with 1713 additions and 93 deletions

View File

@@ -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(&notify))));
}
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;