v0.2.1-pre.003

This commit is contained in:
2026-08-17 18:55:14 +02:00
parent d3fc0c6d69
commit babe7d9f2b
11 changed files with 1007 additions and 22 deletions

View File

@@ -0,0 +1,215 @@
// file: crates/ksp-onchain-transport-lib/src/pool.rs
// version: 1
/// Safe snapshot of the logical HTTP endpoint pool.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HttpTransportPoolSnapshot {
endpoints: std::vec::Vec<crate::HttpEndpointSnapshot>,
}
impl HttpTransportPoolSnapshot {
/// Returns safe endpoint snapshots in configured declaration order.
#[must_use]
pub fn endpoints(&self) -> &[crate::HttpEndpointSnapshot] {
return self.endpoints.as_slice();
}
/// Returns the total number of configured logical endpoints.
#[must_use]
pub fn endpoint_count(&self) -> usize {
return self.endpoints.len();
}
/// Returns the number of endpoints currently eligible for routing.
#[must_use]
pub fn available_endpoint_count(&self) -> usize {
return self.endpoints.iter().filter(|endpoint| endpoint.availability() == crate::HttpEndpointAvailability::Available).count();
}
}
/// Result of one logical endpoint selection.
#[derive(Clone, Debug)]
pub struct HttpEndpointSelection {
client: crate::HttpEndpointClient,
role: crate::HttpRoleName,
request_kind: crate::HttpRequestKind,
priority: u32,
}
impl HttpEndpointSelection {
/// Returns the selected endpoint client.
#[must_use]
pub const fn client(&self) -> &crate::HttpEndpointClient {
return &self.client;
}
/// Returns the selected endpoint identity.
#[must_use]
pub fn endpoint_name(&self) -> &str {
return self.client.name();
}
/// Returns the matched logical role.
#[must_use]
pub const fn role(&self) -> &crate::HttpRoleName {
return &self.role;
}
/// Returns the matched request-kind capability.
#[must_use]
pub const fn request_kind(&self) -> &crate::HttpRequestKind {
return &self.request_kind;
}
/// Returns the selected role priority where lower values are preferred.
#[must_use]
pub const fn priority(&self) -> u32 {
return self.priority;
}
}
/// Shareable logical HTTP endpoint pool with priority routing and per-capability round-robin fairness.
#[derive(Clone)]
pub struct HttpTransportPool {
inner: std::sync::Arc<HttpTransportPoolInner>,
}
struct HttpTransportPoolInner {
clients: std::vec::Vec<crate::HttpEndpointClient>,
cursors: std::sync::Mutex<std::collections::BTreeMap<(std::string::String, std::string::String, u32), usize>>,
}
impl HttpTransportPool {
/// Builds a logical endpoint pool after validating all Transport-owned runtime settings.
pub fn new(settings: crate::HttpTransportSettings) -> ksp_core_lib::Result<Self> {
let validation = settings.validate();
if let std::result::Result::Err(error) = validation {
return std::result::Result::Err(error);
}
let mut clients = std::vec::Vec::with_capacity(settings.endpoints().len());
for endpoint in settings.endpoints() {
let client_result = crate::HttpEndpointClient::new(endpoint.clone());
let client = match client_result {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
clients.push(client);
}
let pool = Self {
inner: std::sync::Arc::new(HttpTransportPoolInner { clients, cursors: std::sync::Mutex::new(std::collections::BTreeMap::new()) }),
};
ksp_logging_lib::debug!(
target: env!("CARGO_PKG_NAME"),
endpoint_count = pool.inner.clients.len(),
available_endpoint_count = pool.snapshot().available_endpoint_count(),
"created logical HTTP endpoint pool"
);
return std::result::Result::Ok(pool);
}
/// Selects an endpoint for one standard audited RPC method.
pub fn select_for_method(&self, role: &crate::HttpRoleName, method: &crate::HttpRpcMethodDescriptor) -> ksp_core_lib::Result<crate::HttpEndpointSelection> {
let support = method.ensure_runtime_supported();
if let std::result::Result::Err(error) = support {
return std::result::Result::Err(error);
}
return self.select_for_request_kind(role, &crate::HttpRequestKind::new(method.request_kind()));
}
/// Selects an endpoint for an open request-kind descriptor, including provider extensions.
pub fn select_for_request_kind(
&self,
role: &crate::HttpRoleName,
request_kind: &crate::HttpRequestKind,
) -> ksp_core_lib::Result<crate::HttpEndpointSelection> {
let mut candidates = std::vec::Vec::new();
for (client_index, client) in self.inner.clients.iter().enumerate() {
let matching_role = client.matching_role(role, request_kind);
if let std::option::Option::Some(matching_role) = matching_role {
candidates.push(PoolCandidate { client_index, priority: matching_role.priority() });
}
}
if candidates.is_empty() {
return selection_failed(role, request_kind);
}
candidates.sort_by_key(|candidate| candidate.priority);
let first_candidate = candidates.first();
let best_priority = match first_candidate {
std::option::Option::Some(candidate) => candidate.priority,
std::option::Option::None => return selection_failed(role, request_kind),
};
let best_tier: std::vec::Vec<PoolCandidate> = candidates.into_iter().take_while(|candidate| candidate.priority == best_priority).collect();
let selected_position = self.next_position(role, request_kind, best_priority, best_tier.len());
let selected = match best_tier.get(selected_position) {
std::option::Option::Some(selected) => selected,
std::option::Option::None => return selection_failed(role, request_kind),
};
let selected_client = self.inner.clients.get(selected.client_index);
let client = match selected_client {
std::option::Option::Some(client) => client.clone(),
std::option::Option::None => return selection_failed(role, request_kind),
};
ksp_logging_lib::debug!(
target: env!("CARGO_PKG_NAME"),
endpoint_name = client.name(),
role = role.as_str(),
request_kind = request_kind.as_str(),
priority = best_priority,
tier_size = best_tier.len(),
"selected logical HTTP endpoint"
);
return std::result::Result::Ok(crate::HttpEndpointSelection {
client,
role: role.clone(),
request_kind: request_kind.clone(),
priority: best_priority,
});
}
/// Returns a safe pool snapshot without endpoint URLs.
#[must_use]
pub fn snapshot(&self) -> crate::HttpTransportPoolSnapshot {
let endpoints = self.inner.clients.iter().map(crate::HttpEndpointClient::snapshot).collect();
return crate::HttpTransportPoolSnapshot { endpoints };
}
fn next_position(&self, role: &crate::HttpRoleName, request_kind: &crate::HttpRequestKind, priority: u32, tier_size: usize) -> usize {
let key = (role.as_str().to_owned(), request_kind.as_str().to_owned(), priority);
let lock_result = self.inner.cursors.lock();
let mut cursors = match lock_result {
std::result::Result::Ok(cursors) => cursors,
std::result::Result::Err(poisoned) => poisoned.into_inner(),
};
let cursor = cursors.entry(key).or_insert(0);
if tier_size == 0 {
return 0;
}
let selected = *cursor % tier_size;
*cursor = (*cursor).wrapping_add(1);
return selected;
}
}
impl std::fmt::Debug for HttpTransportPool {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.debug_struct("HttpTransportPool").field("snapshot", &self.snapshot()).finish();
}
}
#[derive(Clone, Copy)]
struct PoolCandidate {
client_index: usize,
priority: u32,
}
fn selection_failed(role: &crate::HttpRoleName, request_kind: &crate::HttpRequestKind) -> ksp_core_lib::Result<crate::HttpEndpointSelection> {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_ENDPOINT_SELECTION_FAILED, "no HTTP endpoint can satisfy the requested role and request kind")
.with_context("role", role.as_str())
.with_context("request_kind", request_kind.as_str()),
);
}
#[cfg(test)]
#[path = "../unit_tests/pool.rs"]
mod tests;