v0.2.1-pre.003
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 98
|
# version: 99
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib"]
|
members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.2.1-pre.2.fix.1"
|
version = "0.2.1-pre.3"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||||
@@ -18,7 +18,7 @@ fs2 = { version = "^0.4" }
|
|||||||
serde = { version = "^1.0", features = ["derive"] }
|
serde = { version = "^1.0", features = ["derive"] }
|
||||||
serde_json = { version = "^1.0" }
|
serde_json = { version = "^1.0" }
|
||||||
jsonschema = { version = "^0.49", default-features = false }
|
jsonschema = { version = "^0.49", default-features = false }
|
||||||
reqwest = { version = "^0.13", default-features = false }
|
reqwest = { version = "^0.13", default-features = false, features = ["rustls"] }
|
||||||
solana-pubkey = { version = "^4.3", default-features = false }
|
solana-pubkey = { version = "^4.3", default-features = false }
|
||||||
tracing = { version = "^0.1", default-features = false, features = ["std"] }
|
tracing = { version = "^0.1", default-features = false, features = ["std"] }
|
||||||
tracing-subscriber = { version = "^0.3", default-features = false, features = ["fmt", "json", "ansi"] }
|
tracing-subscriber = { version = "^0.3", default-features = false, features = ["fmt", "json", "ansi"] }
|
||||||
|
|||||||
13
ROADMAP.md
13
ROADMAP.md
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: ROADMAP.md -->
|
<!-- file: ROADMAP.md -->
|
||||||
<!-- version: 28 -->
|
<!-- version: 29 -->
|
||||||
|
|
||||||
# Roadmap KSP
|
# Roadmap KSP
|
||||||
|
|
||||||
@@ -40,15 +40,8 @@ Le roadmap décrit les objectifs à atteindre et les grandes étapes prévues. U
|
|||||||
|
|
||||||
### Cadrage
|
### Cadrage
|
||||||
|
|
||||||
- [X] `0.2.0` — Audit bot3, ordre de `0.2.x`, architecture durable et prompt `0.2.1` stabilisés par `0.2.0-rel.001`.
|
- [X] `0.2.0` — Audit bot3, ordre fonctionnel de `0.2.x`, architecture durable, discipline de sizing et pipeline RAW/CORE/DECODE/SPECIALIZED stabilisés.
|
||||||
- [X] `0.2.0-pre.001` — Méthode d'audit, cartographie initiale et matrice provisoire.
|
- [/] `0.2.1` — HTTP foundation en cours : audit/sizing et matrice 52+14 stabilisés ; crate/settings/JSON-RPC/registry acquis ; client endpoint/pool logique et routing priority/fairness acquis ; prochaine étape : limites, concurrence, cooldown, timeout/retry effectif ; 4 canaris, Config standard et clôture restent planifiés.
|
||||||
- [X] `0.2.0-pre.002` — Fixer l'ordre fonctionnel, la discipline de sizing, le pipeline RAW/CORE/DECODE/SPECIALIZED et préparer le prompt `0.2.1`.
|
|
||||||
- [X] `0.2.0-pre.003` — Audit de cohérence final : règles résiduelles supersédées corrigées, fiches `0.2.1+` complétées, TODO bot3 utiles préservés et prompt `0.2.1` finalisé.
|
|
||||||
- [X] `0.2.0-rel.001` — Publication stable du cadrage `0.2.x`; prochaine release : `0.2.1 — ksp-onchain-transport-lib / HTTP Solana foundation`.
|
|
||||||
- [X] `0.2.1-pre.001` — Audit HTTP officiel/bot3, matrice exhaustive, architecture et gate de sizing : scope monolithique refusé, premier split de la couverture HTTP sur `0.2.1`–`0.2.6`, `0.2.1` réduite déclarée raisonnablement clôturable.
|
|
||||||
- [X] `0.2.1-pre.001-fix.001` — Recalibrer le split HTTP sur `0.2.1`–`0.2.4` : `0.2.2` Accounts + Tokens + Cluster, `0.2.3` Transactions, `0.2.4` Blocks + Economics + compliance finale ; autoriser l'enchaînement de plusieurs releases complètement clôturées dans une même session lorsque le sizing le permet.
|
|
||||||
- [X] `0.2.1-pre.002` — Créer la crate `ksp-onchain-transport-lib`, fixer ses settings runtime publics/validation, les envelopes JSON-RPC HTTP, les codes d'erreur, le registre central 52 current + 14 historiques et la base de warnings KSP de statut.
|
|
||||||
- [X] `0.2.1-pre.002-fix.001` — Nettoyer les warnings Clippy/missing-docs révélés par la validation locale de `pre.002`, sans changer son périmètre fonctionnel ni ses contrats publics.
|
|
||||||
|
|
||||||
### Releases fonctionnelles décidées/pressenties
|
### Releases fonctionnelles décidées/pressenties
|
||||||
|
|
||||||
|
|||||||
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;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/src/lib.rs
|
// file: crates/ksp-onchain-transport-lib/src/lib.rs
|
||||||
// version: 1
|
// version: 2
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
@@ -7,14 +7,25 @@
|
|||||||
//! KSP-owned Solana on-chain transport foundation.
|
//! KSP-owned Solana on-chain transport foundation.
|
||||||
//!
|
//!
|
||||||
//! This crate owns runtime HTTP transport settings, Solana HTTP JSON-RPC envelopes and the audited standard method registry. It deliberately remains
|
//! This crate owns runtime HTTP transport settings, Solana HTTP JSON-RPC envelopes and the audited standard method registry. It deliberately remains
|
||||||
//! independent from `ksp-config-lib`, Store and Program layers. Config may later construct these public settings through a one-way adapter, while network
|
//! independent from `ksp-config-lib`, Store and Program layers. Config may later construct these public settings through a one-way adapter. Logical endpoint
|
||||||
//! clients, pools, resilience and typed Solana method adapters are introduced by subsequent `0.2.1` prereleases.
|
//! clients and priority-aware pools are now available, while resilience limits and typed Solana method adapters remain staged by subsequent `0.2.1`
|
||||||
|
//! prereleases.
|
||||||
|
|
||||||
|
mod client;
|
||||||
mod error;
|
mod error;
|
||||||
mod json_rpc;
|
mod json_rpc;
|
||||||
|
mod pool;
|
||||||
mod rpc_method;
|
mod rpc_method;
|
||||||
mod settings;
|
mod settings;
|
||||||
|
|
||||||
|
/// Passive runtime availability reported for one logical HTTP endpoint.
|
||||||
|
pub use self::client::HttpEndpointAvailability;
|
||||||
|
/// Shareable logical HTTP endpoint client owned by KSP Transport.
|
||||||
|
pub use self::client::HttpEndpointClient;
|
||||||
|
/// Safe routing snapshot for one configured endpoint role.
|
||||||
|
pub use self::client::HttpEndpointRoleSnapshot;
|
||||||
|
/// Safe metadata snapshot for one logical HTTP endpoint.
|
||||||
|
pub use self::client::HttpEndpointSnapshot;
|
||||||
/// Error code used when no logical endpoint can satisfy a request.
|
/// Error code used when no logical endpoint can satisfy a request.
|
||||||
pub use self::error::ERROR_CODE_ENDPOINT_SELECTION_FAILED;
|
pub use self::error::ERROR_CODE_ENDPOINT_SELECTION_FAILED;
|
||||||
/// Error code used when an HTTP connection cannot be established.
|
/// Error code used when an HTTP connection cannot be established.
|
||||||
@@ -53,6 +64,12 @@ pub use self::json_rpc::JsonRpcSuccessResponse;
|
|||||||
pub use self::json_rpc::parse_json_rpc_response_text;
|
pub use self::json_rpc::parse_json_rpc_response_text;
|
||||||
/// Validates a decoded JSON value as one JSON-RPC HTTP response.
|
/// Validates a decoded JSON value as one JSON-RPC HTTP response.
|
||||||
pub use self::json_rpc::parse_json_rpc_response_value;
|
pub use self::json_rpc::parse_json_rpc_response_value;
|
||||||
|
/// Result of one logical endpoint selection.
|
||||||
|
pub use self::pool::HttpEndpointSelection;
|
||||||
|
/// Shareable logical HTTP endpoint pool with priority routing and round-robin fairness.
|
||||||
|
pub use self::pool::HttpTransportPool;
|
||||||
|
/// Safe snapshot of the logical HTTP endpoint pool.
|
||||||
|
pub use self::pool::HttpTransportPoolSnapshot;
|
||||||
/// Functional category used by the audited Solana HTTP JSON-RPC registry.
|
/// Functional category used by the audited Solana HTTP JSON-RPC registry.
|
||||||
pub use self::rpc_method::HttpRpcCategory;
|
pub use self::rpc_method::HttpRpcCategory;
|
||||||
/// Release that owns typed KSP coverage for one audited HTTP RPC method.
|
/// Release that owns typed KSP coverage for one audited HTTP RPC method.
|
||||||
|
|||||||
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;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/src/settings.rs
|
// file: crates/ksp-onchain-transport-lib/src/settings.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
/// Runtime HTTP endpoint URL owned by Transport.
|
/// Runtime HTTP endpoint URL owned by Transport.
|
||||||
///
|
///
|
||||||
@@ -443,6 +443,10 @@ impl HttpTransportSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn validate_endpoint_settings(endpoint: &crate::HttpEndpointSettings) -> ksp_core_lib::Result<()> {
|
||||||
|
return validate_endpoint(endpoint, 0);
|
||||||
|
}
|
||||||
|
|
||||||
fn validate_retry(retry: &crate::HttpRetrySettings) -> ksp_core_lib::Result<()> {
|
fn validate_retry(retry: &crate::HttpRetrySettings) -> ksp_core_lib::Result<()> {
|
||||||
if retry.initial_backoff().is_zero() {
|
if retry.initial_backoff().is_zero() {
|
||||||
return invalid_settings("initial retry backoff must be greater than zero", "retry.initial_backoff");
|
return invalid_settings("initial retry backoff must be greater than zero", "retry.initial_backoff");
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
|
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
|
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
|
||||||
|
|
||||||
@@ -61,3 +61,43 @@ fn public_error_codes_share_the_core_error_domain() {
|
|||||||
assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_INVALID_SETTINGS.domain(), "onchain_transport");
|
assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_INVALID_SETTINGS.domain(), "onchain_transport");
|
||||||
assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_RPC_APPLICATION_ERROR.domain(), "onchain_transport");
|
assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_RPC_APPLICATION_ERROR.domain(), "onchain_transport");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn public_pool_contract_selects_a_standard_method_without_exposing_url() {
|
||||||
|
let url = ksp_onchain_transport_lib::HttpEndpointUrl::parse("https://provider.invalid/rpc?token=SECRET-CANARY")
|
||||||
|
.expect("public URL parser must accept HTTPS endpoint");
|
||||||
|
let role = ksp_onchain_transport_lib::HttpEndpointRoleSettings::new(
|
||||||
|
ksp_onchain_transport_lib::HttpRoleName::new("default"),
|
||||||
|
true,
|
||||||
|
std::vec![ksp_onchain_transport_lib::HttpRequestKind::new("get_balance")],
|
||||||
|
10,
|
||||||
|
ksp_onchain_transport_lib::HttpRoleLimits::new(
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let endpoint = ksp_onchain_transport_lib::HttpEndpointSettings::new(
|
||||||
|
"primary",
|
||||||
|
true,
|
||||||
|
ksp_onchain_transport_lib::HttpProviderName::new("provider"),
|
||||||
|
ksp_onchain_transport_lib::HttpClusterName::new("devnet"),
|
||||||
|
url,
|
||||||
|
std::time::Duration::from_secs(2),
|
||||||
|
std::time::Duration::from_secs(10),
|
||||||
|
std::option::Option::Some(8),
|
||||||
|
std::vec![role],
|
||||||
|
);
|
||||||
|
let settings = ksp_onchain_transport_lib::HttpTransportSettings::new(
|
||||||
|
std::vec![endpoint],
|
||||||
|
ksp_onchain_transport_lib::HttpRetrySettings::new(1, std::time::Duration::from_millis(10), std::time::Duration::from_millis(50)),
|
||||||
|
);
|
||||||
|
let pool = ksp_onchain_transport_lib::HttpTransportPool::new(settings).expect("public pool constructor must succeed");
|
||||||
|
let method = ksp_onchain_transport_lib::find_http_rpc_method("getBalance").expect("audited method must exist");
|
||||||
|
let selected = pool.select_for_method(&ksp_onchain_transport_lib::HttpRoleName::new("default"), method).expect("public pool must route audited method");
|
||||||
|
assert_eq!(selected.endpoint_name(), "primary");
|
||||||
|
let rendered = format!("{pool:?}");
|
||||||
|
assert!(!rendered.contains("SECRET-CANARY"));
|
||||||
|
assert!(!rendered.contains("provider.invalid"));
|
||||||
|
}
|
||||||
|
|||||||
49
crates/ksp-onchain-transport-lib/unit_tests/client.rs
Normal file
49
crates/ksp-onchain-transport-lib/unit_tests/client.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// file: crates/ksp-onchain-transport-lib/unit_tests/client.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
fn endpoint(enabled: bool, url_text: &str) -> crate::HttpEndpointSettings {
|
||||||
|
let url = crate::HttpEndpointUrl::parse(url_text).expect("test endpoint URL must parse");
|
||||||
|
let role = crate::HttpEndpointRoleSettings::new(
|
||||||
|
crate::HttpRoleName::new("default"),
|
||||||
|
true,
|
||||||
|
std::vec![crate::HttpRequestKind::wildcard()],
|
||||||
|
10,
|
||||||
|
crate::HttpRoleLimits::new(std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None),
|
||||||
|
);
|
||||||
|
return crate::HttpEndpointSettings::new(
|
||||||
|
"endpoint",
|
||||||
|
enabled,
|
||||||
|
crate::HttpProviderName::new("provider"),
|
||||||
|
crate::HttpClusterName::new("devnet"),
|
||||||
|
url,
|
||||||
|
std::time::Duration::from_secs(1),
|
||||||
|
std::time::Duration::from_secs(2),
|
||||||
|
std::option::Option::Some(4),
|
||||||
|
std::vec![role],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn endpoint_client_snapshot_never_contains_url_or_secret_material() {
|
||||||
|
let client = super::HttpEndpointClient::new(endpoint(true, "https://provider.invalid/rpc?api-key=SECRET-CANARY")).expect("client must build");
|
||||||
|
let snapshot = client.snapshot();
|
||||||
|
let rendered = format!("{snapshot:?} {client:?}");
|
||||||
|
assert_eq!(snapshot.availability(), crate::HttpEndpointAvailability::Available);
|
||||||
|
assert!(!rendered.contains("SECRET-CANARY"));
|
||||||
|
assert!(!rendered.contains("provider.invalid"));
|
||||||
|
assert!(!rendered.contains("https://"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn disabled_endpoint_client_is_visible_but_not_selectable() {
|
||||||
|
let client = super::HttpEndpointClient::new(endpoint(false, "https://api.devnet.solana.com")).expect("disabled client must still build");
|
||||||
|
assert_eq!(client.snapshot().availability(), crate::HttpEndpointAvailability::Disabled);
|
||||||
|
assert!(!client.supports(&crate::HttpRoleName::new("default"), &crate::HttpRequestKind::new("get_balance")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn endpoint_client_matches_exact_and_wildcard_capabilities() {
|
||||||
|
let client = super::HttpEndpointClient::new(endpoint(true, "https://api.devnet.solana.com")).expect("client must build");
|
||||||
|
assert!(client.supports(&crate::HttpRoleName::new("default"), &crate::HttpRequestKind::new("get_balance")));
|
||||||
|
assert!(!client.supports(&crate::HttpRoleName::new("write"), &crate::HttpRequestKind::new("get_balance")));
|
||||||
|
}
|
||||||
167
crates/ksp-onchain-transport-lib/unit_tests/pool.rs
Normal file
167
crates/ksp-onchain-transport-lib/unit_tests/pool.rs
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
// file: crates/ksp-onchain-transport-lib/unit_tests/pool.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
fn role(name: &str, priority: u32, request_kinds: std::vec::Vec<crate::HttpRequestKind>) -> crate::HttpEndpointRoleSettings {
|
||||||
|
return crate::HttpEndpointRoleSettings::new(
|
||||||
|
crate::HttpRoleName::new(name),
|
||||||
|
true,
|
||||||
|
request_kinds,
|
||||||
|
priority,
|
||||||
|
crate::HttpRoleLimits::new(std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn endpoint(name: &str, enabled: bool, priority: u32, request_kinds: std::vec::Vec<crate::HttpRequestKind>) -> crate::HttpEndpointSettings {
|
||||||
|
return crate::HttpEndpointSettings::new(
|
||||||
|
name,
|
||||||
|
enabled,
|
||||||
|
crate::HttpProviderName::new("provider"),
|
||||||
|
crate::HttpClusterName::new("devnet"),
|
||||||
|
crate::HttpEndpointUrl::parse(format!("https://{name}.invalid/rpc?token=SECRET-CANARY")).expect("test URL must parse"),
|
||||||
|
std::time::Duration::from_secs(1),
|
||||||
|
std::time::Duration::from_secs(2),
|
||||||
|
std::option::Option::Some(4),
|
||||||
|
std::vec![role("default", priority, request_kinds)],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn settings(endpoints: std::vec::Vec<crate::HttpEndpointSettings>) -> crate::HttpTransportSettings {
|
||||||
|
return crate::HttpTransportSettings::new(
|
||||||
|
endpoints,
|
||||||
|
crate::HttpRetrySettings::new(2, std::time::Duration::from_millis(10), std::time::Duration::from_millis(50)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pool_prefers_lowest_priority_tier() {
|
||||||
|
let pool = super::HttpTransportPool::new(settings(std::vec![
|
||||||
|
endpoint("secondary", true, 20, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||||
|
endpoint("primary", true, 10, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||||
|
]))
|
||||||
|
.expect("pool must build");
|
||||||
|
let selection = pool
|
||||||
|
.select_for_request_kind(&crate::HttpRoleName::new("default"), &crate::HttpRequestKind::new("get_balance"))
|
||||||
|
.expect("selection must succeed");
|
||||||
|
assert_eq!(selection.endpoint_name(), "primary");
|
||||||
|
assert_eq!(selection.priority(), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pool_round_robins_fairly_inside_best_priority_tier() {
|
||||||
|
let pool = super::HttpTransportPool::new(settings(std::vec![
|
||||||
|
endpoint("one", true, 10, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||||
|
endpoint("two", true, 10, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||||
|
endpoint("fallback", true, 20, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||||
|
]))
|
||||||
|
.expect("pool must build");
|
||||||
|
let role = crate::HttpRoleName::new("default");
|
||||||
|
let kind = crate::HttpRequestKind::new("get_balance");
|
||||||
|
let first = pool.select_for_request_kind(&role, &kind).expect("first selection must succeed");
|
||||||
|
let second = pool.select_for_request_kind(&role, &kind).expect("second selection must succeed");
|
||||||
|
let third = pool.select_for_request_kind(&role, &kind).expect("third selection must succeed");
|
||||||
|
assert_eq!(first.endpoint_name(), "one");
|
||||||
|
assert_eq!(second.endpoint_name(), "two");
|
||||||
|
assert_eq!(third.endpoint_name(), "one");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn disabled_best_priority_endpoint_falls_back_to_next_tier() {
|
||||||
|
let pool = super::HttpTransportPool::new(settings(std::vec![
|
||||||
|
endpoint("disabled-primary", false, 1, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||||
|
endpoint("fallback", true, 20, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||||
|
]))
|
||||||
|
.expect("pool must build");
|
||||||
|
let selection = pool
|
||||||
|
.select_for_request_kind(&crate::HttpRoleName::new("default"), &crate::HttpRequestKind::new("get_balance"))
|
||||||
|
.expect("fallback must be selected");
|
||||||
|
assert_eq!(selection.endpoint_name(), "fallback");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pool_filters_role_and_capability_before_priority() {
|
||||||
|
let pool = super::HttpTransportPool::new(settings(std::vec![
|
||||||
|
endpoint("wrong-capability", true, 1, std::vec![crate::HttpRequestKind::new("send_transaction")]),
|
||||||
|
endpoint("matching", true, 50, std::vec![crate::HttpRequestKind::new("get_balance")]),
|
||||||
|
]))
|
||||||
|
.expect("pool must build");
|
||||||
|
let selection = pool
|
||||||
|
.select_for_request_kind(&crate::HttpRoleName::new("default"), &crate::HttpRequestKind::new("get_balance"))
|
||||||
|
.expect("matching capability must be selected");
|
||||||
|
assert_eq!(selection.endpoint_name(), "matching");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pool_returns_structured_error_when_no_endpoint_matches() {
|
||||||
|
let pool = super::HttpTransportPool::new(settings(std::vec![endpoint("read-only", true, 10, std::vec![crate::HttpRequestKind::new("get_balance")],)]))
|
||||||
|
.expect("pool must build");
|
||||||
|
let error = pool
|
||||||
|
.select_for_request_kind(&crate::HttpRoleName::new("default"), &crate::HttpRequestKind::new("send_transaction"))
|
||||||
|
.expect_err("unsupported request kind must fail selection");
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_ENDPOINT_SELECTION_FAILED);
|
||||||
|
assert!(!format!("{error:?}").contains("SECRET-CANARY"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn standard_method_selection_uses_registry_request_kind() {
|
||||||
|
let pool = super::HttpTransportPool::new(settings(std::vec![endpoint("balance", true, 10, std::vec![crate::HttpRequestKind::new("get_balance")],)]))
|
||||||
|
.expect("pool must build");
|
||||||
|
let method = crate::find_http_rpc_method("getBalance").expect("audited method must exist");
|
||||||
|
let selection = pool.select_for_method(&crate::HttpRoleName::new("default"), method).expect("standard method must route");
|
||||||
|
assert_eq!(selection.request_kind().as_str(), "get_balance");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pool_snapshot_is_safe_and_preserves_disabled_endpoints() {
|
||||||
|
let pool = super::HttpTransportPool::new(settings(std::vec![
|
||||||
|
endpoint("enabled", true, 10, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||||
|
endpoint("disabled", false, 10, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||||
|
]))
|
||||||
|
.expect("pool must build");
|
||||||
|
let snapshot = pool.snapshot();
|
||||||
|
let rendered = format!("{snapshot:?} {pool:?}");
|
||||||
|
assert_eq!(snapshot.endpoint_count(), 2);
|
||||||
|
assert_eq!(snapshot.available_endpoint_count(), 1);
|
||||||
|
assert!(!rendered.contains("SECRET-CANARY"));
|
||||||
|
assert!(!rendered.contains(".invalid/rpc"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn disabled_role_is_excluded_before_priority_selection() {
|
||||||
|
let base = endpoint("disabled-role", true, 1, std::vec![crate::HttpRequestKind::wildcard()]);
|
||||||
|
let disabled_role = crate::HttpEndpointRoleSettings::new(
|
||||||
|
crate::HttpRoleName::new("default"),
|
||||||
|
false,
|
||||||
|
std::vec![crate::HttpRequestKind::wildcard()],
|
||||||
|
1,
|
||||||
|
crate::HttpRoleLimits::new(std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None),
|
||||||
|
);
|
||||||
|
let disabled_role_endpoint = crate::HttpEndpointSettings::new(
|
||||||
|
base.name(),
|
||||||
|
true,
|
||||||
|
base.provider().clone(),
|
||||||
|
base.cluster().clone(),
|
||||||
|
base.url().clone(),
|
||||||
|
base.connect_timeout(),
|
||||||
|
base.request_timeout(),
|
||||||
|
base.max_idle_connections_per_host(),
|
||||||
|
std::vec![disabled_role],
|
||||||
|
);
|
||||||
|
let pool = super::HttpTransportPool::new(settings(std::vec![
|
||||||
|
disabled_role_endpoint,
|
||||||
|
endpoint("fallback", true, 20, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||||
|
]))
|
||||||
|
.expect("pool must build");
|
||||||
|
let selection = pool
|
||||||
|
.select_for_request_kind(&crate::HttpRoleName::new("default"), &crate::HttpRequestKind::new("get_balance"))
|
||||||
|
.expect("enabled fallback role must be selected");
|
||||||
|
assert_eq!(selection.endpoint_name(), "fallback");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn removed_standard_method_is_rejected_before_endpoint_routing() {
|
||||||
|
let pool = super::HttpTransportPool::new(settings(std::vec![endpoint("wildcard", true, 10, std::vec![crate::HttpRequestKind::wildcard()],)]))
|
||||||
|
.expect("pool must build");
|
||||||
|
let method = crate::find_http_rpc_method("confirmTransaction").expect("historical method must exist");
|
||||||
|
let error = pool.select_for_method(&crate::HttpRoleName::new("default"), method).expect_err("removed standard method must be rejected before routing");
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_METHOD_REMOVED);
|
||||||
|
}
|
||||||
196
deltas/0.2.1/pre.003.md
Normal file
196
deltas/0.2.1/pre.003.md
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
<!-- file: deltas/0.2.1/pre.003.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `v0.2.1-pre.003`
|
||||||
|
|
||||||
|
## Base
|
||||||
|
|
||||||
|
Base attendue : `v0.2.1-pre.002-fix.001`, validée localement avant ouverture de cette tranche.
|
||||||
|
|
||||||
|
Version Cargo cible :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.1-pre.3
|
||||||
|
```
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Matérialiser la première couche de client/routing HTTP de `ksp-onchain-transport-lib` sans encore exécuter de méthode JSON-RPC :
|
||||||
|
|
||||||
|
- client endpoint logique autour de `reqwest::Client` ;
|
||||||
|
- pool logique KSP ;
|
||||||
|
- matching rôle/capability ;
|
||||||
|
- priorité globale ;
|
||||||
|
- round-robin équitable dans un même tier ;
|
||||||
|
- fallback lorsque les candidats plus prioritaires ne sont pas sélectionnables ;
|
||||||
|
- snapshots sûrs sans URL ;
|
||||||
|
- préparation des états passifs nécessaires à la résilience de `pre.004`.
|
||||||
|
|
||||||
|
Cette tranche corrige également l'usage de `ROADMAP.md` afin de respecter son contrat existant : le ROADMAP décrit l'état synthétique et la planification majeure, pas l'historique des prereleases/fixes.
|
||||||
|
|
||||||
|
## Modifications
|
||||||
|
|
||||||
|
### Workspace
|
||||||
|
|
||||||
|
- `workspace.package.version` passe de `0.2.1-pre.2.fix.1` à `0.2.1-pre.3` ;
|
||||||
|
- `reqwest` reste centralisé sous `[workspace.dependencies]`, avec `default-features = false` ;
|
||||||
|
- activation explicite de la feature `rustls`, nécessaire à la construction réelle de clients HTTPS dans cette tranche ;
|
||||||
|
- aucune feature `json` n'est ajoutée : les envelopes JSON-RPC restent possédées par KSP via `serde_json` ;
|
||||||
|
- aucune dépendance Tokio directe n'est ajoutée à Transport dans cette tranche.
|
||||||
|
|
||||||
|
### `HttpEndpointClient`
|
||||||
|
|
||||||
|
Nouveau client endpoint logique :
|
||||||
|
|
||||||
|
- encapsule un `reqwest::Client` partageable ;
|
||||||
|
- applique `connect_timeout`, `request_timeout` et `max_idle_connections_per_host` depuis les settings KSP ;
|
||||||
|
- désactive les redirects automatiques ;
|
||||||
|
- désactive les proxies système/environnement implicites ;
|
||||||
|
- fixe un `User-Agent` KSP `ksp-onchain-transport-lib/<version>` ;
|
||||||
|
- conserve l'URL hors de toute surface `Debug`/snapshot ;
|
||||||
|
- expose uniquement identité, provider, cluster, état et capability matching sûrs.
|
||||||
|
|
||||||
|
### Snapshots et état passif
|
||||||
|
|
||||||
|
Nouveaux contrats publics :
|
||||||
|
|
||||||
|
```text
|
||||||
|
HttpEndpointAvailability
|
||||||
|
HttpEndpointRoleSnapshot
|
||||||
|
HttpEndpointSnapshot
|
||||||
|
HttpTransportPoolSnapshot
|
||||||
|
```
|
||||||
|
|
||||||
|
Les snapshots ne contiennent jamais l'URL endpoint.
|
||||||
|
|
||||||
|
`HttpEndpointAvailability` réserve :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Disabled
|
||||||
|
Available
|
||||||
|
Degraded
|
||||||
|
RateLimited
|
||||||
|
```
|
||||||
|
|
||||||
|
`pre.003` ne produit activement que `Disabled` et `Available`. Les transitions `Degraded` / `RateLimited` appartiennent à `pre.004` avec cooldown, limiter et observations runtime.
|
||||||
|
|
||||||
|
### `HttpTransportPool`
|
||||||
|
|
||||||
|
Nouveaux contrats publics :
|
||||||
|
|
||||||
|
```text
|
||||||
|
HttpTransportPool
|
||||||
|
HttpEndpointSelection
|
||||||
|
```
|
||||||
|
|
||||||
|
Algorithme concret de `pre.003` :
|
||||||
|
|
||||||
|
1. valider les settings Transport ;
|
||||||
|
2. construire un client logique par endpoint ;
|
||||||
|
3. exclure les endpoints disabled ;
|
||||||
|
4. rechercher un rôle exact enabled ;
|
||||||
|
5. exiger la capability exacte ou `*` ;
|
||||||
|
6. choisir la plus faible priorité numérique ;
|
||||||
|
7. appliquer un round-robin par couple rôle/request-kind dans ce meilleur tier ;
|
||||||
|
8. utiliser un tier moins prioritaire lorsque les candidats plus prioritaires ne sont pas sélectionnables ;
|
||||||
|
9. retourner `endpoint_selection_failed` si aucun candidat n'existe.
|
||||||
|
|
||||||
|
Le mutex synchrone du pool protège uniquement les curseurs de fairness et ne couvre aucune I/O ni aucun `await` réseau.
|
||||||
|
|
||||||
|
`select_for_method()` dérive le `request_kind` depuis le registre central et appelle `ensure_runtime_supported()` avant sélection ; une méthode historique `Removed` ne peut donc pas être routée comme méthode standard.
|
||||||
|
|
||||||
|
### ROADMAP
|
||||||
|
|
||||||
|
`ROADMAP.md` est ramené à son rôle défini par `FILE_CONTRACTS.md` :
|
||||||
|
|
||||||
|
- suppression des lignes servant de journal `0.2.1-pre.*` / `fix.*` ;
|
||||||
|
- conservation d'un état synthétique de `0.2.0` ;
|
||||||
|
- état synthétique courant de `0.2.1` ;
|
||||||
|
- conservation des releases fonctionnelles `0.2.1+` et de leur planification majeure.
|
||||||
|
|
||||||
|
Aucune règle normative nouvelle n'est nécessaire : cette correction applique le contrat `ROADMAP.md` déjà documenté.
|
||||||
|
|
||||||
|
### Plan `008`
|
||||||
|
|
||||||
|
Le plan est synchronisé avec :
|
||||||
|
|
||||||
|
- la feature TLS réellement retenue ;
|
||||||
|
- la politique client `reqwest` ;
|
||||||
|
- le statut réalisé de `pre.003` ;
|
||||||
|
- l'état exact après la tranche ;
|
||||||
|
- le report explicite des limiteurs/concurrence/cooldown/retry effectifs à `pre.004`.
|
||||||
|
|
||||||
|
## Tests ajoutés
|
||||||
|
|
||||||
|
La crate Transport passe de 40 à **53 tests déclarés**.
|
||||||
|
|
||||||
|
Nouvelles preuves :
|
||||||
|
|
||||||
|
- snapshot client sans URL/secret ;
|
||||||
|
- endpoint disabled visible mais non sélectionnable ;
|
||||||
|
- matching rôle/wildcard capability ;
|
||||||
|
- priorité globale ;
|
||||||
|
- round-robin dans le meilleur tier ;
|
||||||
|
- fallback depuis un endpoint plus prioritaire disabled ;
|
||||||
|
- filtrage capability avant priorité ;
|
||||||
|
- erreur structurée lorsque rien ne correspond ;
|
||||||
|
- routing d'une méthode standard via son descriptor ;
|
||||||
|
- snapshot pool sûr conservant les endpoints disabled ;
|
||||||
|
- consommation du nouveau pool depuis l'API publique.
|
||||||
|
|
||||||
|
## Hors périmètre conservé
|
||||||
|
|
||||||
|
Restent à `pre.004` :
|
||||||
|
|
||||||
|
- token bucket RPS/burst ;
|
||||||
|
- semaphore/max concurrent ;
|
||||||
|
- cooldown après `429` ;
|
||||||
|
- deadline effective de sélection/exécution ;
|
||||||
|
- retry/backoff effectif ;
|
||||||
|
- classification des erreurs `reqwest` pendant une requête ;
|
||||||
|
- transitions runtime `Degraded` / `RateLimited`.
|
||||||
|
|
||||||
|
Restent à `pre.005+` :
|
||||||
|
|
||||||
|
- exécution JSON-RPC HTTP ;
|
||||||
|
- méthodes canari typées ;
|
||||||
|
- document Config standard et adapter ;
|
||||||
|
- smoke tests réseau.
|
||||||
|
|
||||||
|
## Sources externes revérifiées
|
||||||
|
|
||||||
|
Pour `reqwest 0.13` :
|
||||||
|
|
||||||
|
- dépôt/documentation officielle `reqwest` : rustls est le backend TLS de référence actuel ;
|
||||||
|
- changelog `reqwest` : en `0.13`, la feature historique `rustls-tls` a été renommée `rustls` ;
|
||||||
|
- `ClientBuilder` officiel : `connect_timeout`, `timeout`, `pool_max_idle_per_host`, `redirect` et `no_proxy` sont disponibles sur le client async retenu.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Non exécutée dans le sandbox de génération : `cargo` et `rustc` n'y sont pas installés.
|
||||||
|
|
||||||
|
À exécuter après application :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test --workspace
|
||||||
|
cargo tree -p ksp-onchain-transport-lib
|
||||||
|
cargo tree -p ksp-onchain-transport-lib -d
|
||||||
|
cargo tree -p ksp-onchain-transport-lib -e features
|
||||||
|
cargo tree -p ksp-onchain-transport-lib -e normal
|
||||||
|
```
|
||||||
|
|
||||||
|
Les quatre `cargo tree` doivent être rejoués dans cette tranche car le feature-set de `reqwest` change avec l'activation de `rustls`.
|
||||||
|
|
||||||
|
## Suite
|
||||||
|
|
||||||
|
Tranche suivante prévue :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.1-pre.004
|
||||||
|
```
|
||||||
|
|
||||||
|
Périmètre : RPS/burst, concurrence, cooldown/429, timeout/deadline et retry/backoff borné, avec respect strict de `TransportRetryClass` et interdiction de resend après dispatch ambigu.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md -->
|
<!-- file: docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md -->
|
||||||
<!-- version: 3 -->
|
<!-- version: 4 -->
|
||||||
|
|
||||||
# `0.2.1-pre.001` — plan `ksp-onchain-transport-lib` HTTP Solana foundation
|
# `0.2.1-pre.001` — plan `ksp-onchain-transport-lib` HTTP Solana foundation
|
||||||
|
|
||||||
@@ -498,6 +498,18 @@ Pour la surface standard connue, l'exécuteur consulte le registry. Une méthode
|
|||||||
|
|
||||||
Le pool sélectionne un **endpoint/client HTTP logique**. Le pooling de sockets/keep-alive reste la responsabilité du client `reqwest` sous-jacent.
|
Le pool sélectionne un **endpoint/client HTTP logique**. Le pooling de sockets/keep-alive reste la responsabilité du client `reqwest` sous-jacent.
|
||||||
|
|
||||||
|
### 12.1.1 Politique client concrétisée par `pre.003`
|
||||||
|
|
||||||
|
Chaque endpoint construit un `reqwest::Client` partageable qui conserve le pooling de sockets sous-jacent. Le builder KSP applique les `connect_timeout`, `request_timeout` et `max_idle_connections_per_host` déjà possédés par les settings.
|
||||||
|
|
||||||
|
Décisions de sécurité/déterminisme :
|
||||||
|
|
||||||
|
- backend HTTPS : feature `reqwest/rustls` explicite ;
|
||||||
|
- redirects automatiques désactivés : une URL provider éventuellement porteuse de credential n'est jamais redirigée implicitement vers une autre destination ;
|
||||||
|
- proxies système/environnement implicites désactivés via le builder `reqwest`; une future prise en charge de proxy devra être un contrat Config/Transport explicite ;
|
||||||
|
- `User-Agent` KSP stable : `ksp-onchain-transport-lib/<version>` ;
|
||||||
|
- URL conservée uniquement dans les settings privés du client logique et absente des snapshots/`Debug` publics.
|
||||||
|
|
||||||
### 12.2 Algorithme de sélection
|
### 12.2 Algorithme de sélection
|
||||||
|
|
||||||
Pour une requête :
|
Pour une requête :
|
||||||
@@ -706,7 +718,7 @@ ksp-core-lib path/workspace
|
|||||||
ksp-logging-lib path/workspace
|
ksp-logging-lib path/workspace
|
||||||
```
|
```
|
||||||
|
|
||||||
`pre.002` utilise `reqwest` uniquement pour le parsing/validation robuste de `HttpEndpointUrl`; aucune requête réseau n'est encore créée. Conformément à `RUST-DEP-001` / `RUST-DEP-003`, aucune feature TLS/JSON/client ni dépendance Tokio locale n'est activée par anticipation. Les features réellement nécessaires au client async sont décidées et ajoutées dans `pre.003`, lorsque le chemin de compilation HTTP existe. Le même principe reporte `tokio` à la première tranche qui utilise effectivement ses primitives runtime/sync.
|
`pre.002` utilisait `reqwest` uniquement pour le parsing/validation robuste de `HttpEndpointUrl`. `pre.003` crée réellement les clients HTTP logiques et active donc la feature `rustls` de `reqwest 0.13` au workspace, toujours avec `default-features = false`. Aucune feature `json` n'est nécessaire : KSP possède déjà son encodage JSON-RPC via `serde_json`. Aucune dépendance Tokio directe n'est ajoutée tant que `pre.004` n'utilise pas effectivement ses primitives de concurrence/attente.
|
||||||
|
|
||||||
Ne pas ajouter :
|
Ne pas ajouter :
|
||||||
|
|
||||||
@@ -859,7 +871,7 @@ La release peut devenir stable seulement si :
|
|||||||
|-----------|----------------------------------------------------------------------------------------------------------------------|
|
|-----------|----------------------------------------------------------------------------------------------------------------------|
|
||||||
| `pre.001` | audit KSP + bot3 + docs officielles, matrice 52+14, architecture, split et sizing |
|
| `pre.001` | audit KSP + bot3 + docs officielles, matrice 52+14, architecture, split et sizing |
|
||||||
| `pre.002` | **réalisé** : crate/workspace, codes erreur, settings/validation, JSON-RPC, descriptors/status, base Logging |
|
| `pre.002` | **réalisé** : crate/workspace, codes erreur, settings/validation, JSON-RPC, descriptors/status, base Logging |
|
||||||
| `pre.003` | endpoint client + pool logique + rôles/capabilities + priorité/fairness/fallback + snapshots sûrs |
|
| `pre.003` | **réalisé** : endpoint client + pool logique + rôles/capabilities + priorité/fairness/fallback + snapshots sûrs |
|
||||||
| `pre.004` | RPS/burst/concurrence/cooldown + timeout + retry/backoff + classification retry/no-resend |
|
| `pre.004` | RPS/burst/concurrence/cooldown + timeout + retry/backoff + classification retry/no-resend |
|
||||||
| `pre.005` | méthodes canari `getHealth`, `getVersion`, `getGenesisHash`, `getBalance` + fixtures déterministes |
|
| `pre.005` | méthodes canari `getHealth`, `getVersion`, `getGenesisHash`, `getBalance` + fixtures déterministes |
|
||||||
| `pre.006` | `std.transport` schema/document/example + registry Config + adapter Config -> Transport + sensibilité/env tests |
|
| `pre.006` | `std.transport` schema/document/example + registry Config + adapter Config -> Transport + sensibilité/env tests |
|
||||||
@@ -882,7 +894,25 @@ Ce découpage est révisable si une tranche dépasse le budget ; la release réd
|
|||||||
- contrôle central `ensure_runtime_supported()` : warning KSP pour surfaces non stables supportées, warning + `method_removed` pour les historiques supprimées ;
|
- contrôle central `ensure_runtime_supported()` : warning KSP pour surfaces non stables supportées, warning + `method_removed` pour les historiques supprimées ;
|
||||||
- 40 tests Rust ajoutés (unitaires + intégration), dont redaction, invariants JSON-RPC, matrice 4/22/11/15 et canary de firewall du manifest.
|
- 40 tests Rust ajoutés (unitaires + intégration), dont redaction, invariants JSON-RPC, matrice 4/22/11/15 et canary de firewall du manifest.
|
||||||
|
|
||||||
Les éléments suivants restent volontairement à `pre.003+` : construction `reqwest::Client`, clients logiques, pool, sélection/rôles/fallback, Tokio runtime/sync, TLS, rate limiting, concurrence et retry effectif.
|
Les éléments annoncés pour `pre.003` dans cet état historique sont désormais concrétisés au §22.2 ; rate limiting, concurrence et retry effectif restent reportés.
|
||||||
|
|
||||||
|
### 22.2 État après `0.2.1-pre.003`
|
||||||
|
|
||||||
|
`pre.003` matérialise la première couche de routing HTTP sans encore exécuter de JSON-RPC :
|
||||||
|
|
||||||
|
- `workspace.package.version = 0.2.1-pre.3` ;
|
||||||
|
- `reqwest` reste `default-features = false` et active uniquement `rustls` pour rendre les endpoints HTTPS réellement constructibles ;
|
||||||
|
- `HttpEndpointClient` encapsule un `reqwest::Client`, applique connect/request timeout et limite idle-per-host, désactive redirect et proxy système implicites, et n'expose jamais l'URL dans `Debug`/snapshot ;
|
||||||
|
- `HttpEndpointSnapshot`, `HttpEndpointRoleSnapshot` et `HttpTransportPoolSnapshot` exposent uniquement identité/provider/cluster/routing/status sûrs ;
|
||||||
|
- `HttpTransportPool` valide les settings puis construit un client logique par endpoint ;
|
||||||
|
- sélection exacte rôle + capability, wildcard `*`, priorité globale croissante et round-robin par rôle/request-kind dans le meilleur tier ;
|
||||||
|
- endpoint ou rôle disabled exclus ; un tier inférieur devient donc fallback lorsqu'aucun candidat du tier supérieur n'est sélectionnable ;
|
||||||
|
- `select_for_method` dérive la capability depuis le registre RPC central et refuse une méthode historique `Removed` avant routing ;
|
||||||
|
- aucune mutex synchrone ne couvre un `await` réseau : le seul verrou actuel protège brièvement les curseurs de fairness, avant toute I/O ;
|
||||||
|
- le contrat d'availability réserve `Disabled`, `Available`, `Degraded`, `RateLimited`; `pre.003` ne produit que les deux premiers, les transitions runtime appartenant à `pre.004`.
|
||||||
|
- la suite Transport compte désormais 53 tests déclarés, dont les nouvelles preuves de redaction client/pool, priorité, fairness, fallback, capability et contrat public du pool.
|
||||||
|
|
||||||
|
Restent volontairement à `pre.004` : token bucket RPS/burst, semaphore de concurrence, cooldown/429, deadline effective, retry/backoff et mutations passives `Degraded/RateLimited`.
|
||||||
|
|
||||||
## 23. Séquence `0.2.x` recalibrée
|
## 23. Séquence `0.2.x` recalibrée
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user