v0.2.1-pre.003
This commit is contained in:
215
crates/ksp-onchain-transport-lib/src/pool.rs
Normal file
215
crates/ksp-onchain-transport-lib/src/pool.rs
Normal 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;
|
||||
Reference in New Issue
Block a user