v0.2.1-pre.003
This commit is contained in:
274
crates/ksp-onchain-transport-lib/src/client.rs
Normal file
274
crates/ksp-onchain-transport-lib/src/client.rs
Normal file
@@ -0,0 +1,274 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/client.rs
|
||||
// version: 1
|
||||
|
||||
/// Passive runtime availability reported for one logical HTTP endpoint.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum HttpEndpointAvailability {
|
||||
/// The endpoint is administratively disabled and cannot be selected.
|
||||
Disabled,
|
||||
/// The endpoint is enabled and currently eligible for selection.
|
||||
Available,
|
||||
/// The endpoint is enabled but temporarily degraded by runtime observations.
|
||||
Degraded,
|
||||
/// The endpoint is enabled but temporarily excluded after provider rate limiting.
|
||||
RateLimited,
|
||||
}
|
||||
|
||||
/// Safe routing 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,
|
||||
}
|
||||
|
||||
impl HttpEndpointRoleSnapshot {
|
||||
/// Returns the logical role name.
|
||||
#[must_use]
|
||||
pub fn role(&self) -> &str {
|
||||
return self.role.as_str();
|
||||
}
|
||||
|
||||
/// Returns whether the role is enabled.
|
||||
#[must_use]
|
||||
pub const fn enabled(&self) -> bool {
|
||||
return self.enabled;
|
||||
}
|
||||
|
||||
/// Returns request-kind descriptors accepted by the role.
|
||||
#[must_use]
|
||||
pub fn request_kinds(&self) -> &[std::string::String] {
|
||||
return self.request_kinds.as_slice();
|
||||
}
|
||||
|
||||
/// Returns the routing priority where lower values are preferred.
|
||||
#[must_use]
|
||||
pub const fn priority(&self) -> u32 {
|
||||
return self.priority;
|
||||
}
|
||||
}
|
||||
|
||||
/// Safe metadata snapshot for one logical HTTP endpoint.
|
||||
///
|
||||
/// Endpoint URLs are intentionally absent because they can contain provider credentials.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct HttpEndpointSnapshot {
|
||||
name: std::string::String,
|
||||
provider: std::string::String,
|
||||
cluster: std::string::String,
|
||||
enabled: bool,
|
||||
availability: crate::HttpEndpointAvailability,
|
||||
roles: std::vec::Vec<crate::HttpEndpointRoleSnapshot>,
|
||||
}
|
||||
|
||||
impl HttpEndpointSnapshot {
|
||||
/// Returns the configured endpoint identity.
|
||||
#[must_use]
|
||||
pub fn name(&self) -> &str {
|
||||
return self.name.as_str();
|
||||
}
|
||||
|
||||
/// Returns the provider descriptor.
|
||||
#[must_use]
|
||||
pub fn provider(&self) -> &str {
|
||||
return self.provider.as_str();
|
||||
}
|
||||
|
||||
/// Returns the cluster descriptor.
|
||||
#[must_use]
|
||||
pub fn cluster(&self) -> &str {
|
||||
return self.cluster.as_str();
|
||||
}
|
||||
|
||||
/// Returns whether the endpoint is administratively enabled.
|
||||
#[must_use]
|
||||
pub const fn enabled(&self) -> bool {
|
||||
return self.enabled;
|
||||
}
|
||||
|
||||
/// Returns the passive runtime availability.
|
||||
#[must_use]
|
||||
pub const fn availability(&self) -> crate::HttpEndpointAvailability {
|
||||
return self.availability;
|
||||
}
|
||||
|
||||
/// Returns safe role snapshots in declaration order.
|
||||
#[must_use]
|
||||
pub fn roles(&self) -> &[crate::HttpEndpointRoleSnapshot] {
|
||||
return self.roles.as_slice();
|
||||
}
|
||||
}
|
||||
|
||||
/// Shareable logical HTTP endpoint client owned by KSP Transport.
|
||||
///
|
||||
/// The underlying `reqwest::Client` owns socket pooling. KSP keeps the configured URL private from diagnostics and exposes only safe routing metadata.
|
||||
#[derive(Clone)]
|
||||
pub struct HttpEndpointClient {
|
||||
inner: std::sync::Arc<HttpEndpointClientInner>,
|
||||
}
|
||||
|
||||
struct HttpEndpointClientInner {
|
||||
settings: crate::HttpEndpointSettings,
|
||||
_client: reqwest::Client,
|
||||
availability: std::sync::atomic::AtomicU8,
|
||||
}
|
||||
|
||||
impl HttpEndpointClient {
|
||||
/// Builds one logical endpoint client from KSP-owned runtime settings.
|
||||
pub fn new(settings: crate::HttpEndpointSettings) -> 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);
|
||||
}
|
||||
let client_result = build_reqwest_client(&settings);
|
||||
let client = match client_result {
|
||||
std::result::Result::Ok(client) => client,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_HTTP_CONNECTION_FAILED, "HTTP endpoint client could not be initialized")
|
||||
.with_context("endpoint_name", settings.name())
|
||||
.with_source(error),
|
||||
);
|
||||
},
|
||||
};
|
||||
let availability = if settings.enabled() { AVAILABILITY_AVAILABLE } else { AVAILABILITY_DISABLED };
|
||||
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(),
|
||||
"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) }),
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the endpoint identity used for safe diagnostics and routing.
|
||||
#[must_use]
|
||||
pub fn name(&self) -> &str {
|
||||
return self.inner.settings.name();
|
||||
}
|
||||
|
||||
/// Returns the provider descriptor.
|
||||
#[must_use]
|
||||
pub fn provider(&self) -> &crate::HttpProviderName {
|
||||
return self.inner.settings.provider();
|
||||
}
|
||||
|
||||
/// Returns the cluster descriptor.
|
||||
#[must_use]
|
||||
pub fn cluster(&self) -> &crate::HttpClusterName {
|
||||
return self.inner.settings.cluster();
|
||||
}
|
||||
|
||||
/// Returns whether the endpoint is administratively enabled.
|
||||
#[must_use]
|
||||
pub fn enabled(&self) -> bool {
|
||||
return self.inner.settings.enabled();
|
||||
}
|
||||
|
||||
/// Returns whether one enabled role can serve the requested capability.
|
||||
#[must_use]
|
||||
pub fn supports(&self, role: &crate::HttpRoleName, request_kind: &crate::HttpRequestKind) -> bool {
|
||||
return self.matching_role(role, request_kind).is_some();
|
||||
}
|
||||
|
||||
/// Returns a safe endpoint snapshot with no URL or provider credential material.
|
||||
#[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() {
|
||||
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(),
|
||||
});
|
||||
}
|
||||
return crate::HttpEndpointSnapshot {
|
||||
name: self.name().to_owned(),
|
||||
provider: self.provider().as_str().to_owned(),
|
||||
cluster: self.cluster().as_str().to_owned(),
|
||||
enabled: self.enabled(),
|
||||
availability: self.availability(),
|
||||
roles,
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn matching_role<'a>(
|
||||
&'a self,
|
||||
role: &crate::HttpRoleName,
|
||||
request_kind: &crate::HttpRequestKind,
|
||||
) -> std::option::Option<&'a crate::HttpEndpointRoleSettings> {
|
||||
if !self.is_selectable() {
|
||||
return std::option::Option::None;
|
||||
}
|
||||
for candidate_role in self.inner.settings.roles() {
|
||||
if !candidate_role.enabled() || candidate_role.role() != role {
|
||||
continue;
|
||||
}
|
||||
for capability in candidate_role.request_kinds() {
|
||||
if capability.is_wildcard() || capability == request_kind {
|
||||
return std::option::Option::Some(candidate_role);
|
||||
}
|
||||
}
|
||||
}
|
||||
return std::option::Option::None;
|
||||
}
|
||||
|
||||
fn is_selectable(&self) -> bool {
|
||||
return self.enabled() && self.availability() == crate::HttpEndpointAvailability::Available;
|
||||
}
|
||||
|
||||
fn availability(&self) -> crate::HttpEndpointAvailability {
|
||||
return availability_from_code(self.inner.availability.load(std::sync::atomic::Ordering::Relaxed));
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for HttpEndpointClient {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter.debug_struct("HttpEndpointClient").field("snapshot", &self.snapshot()).finish();
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
.timeout(settings.request_timeout())
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.no_proxy()
|
||||
.user_agent(concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")));
|
||||
if let std::option::Option::Some(max_idle) = settings.max_idle_connections_per_host() {
|
||||
builder = builder.pool_max_idle_per_host(max_idle);
|
||||
}
|
||||
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