191 lines
6.8 KiB
Rust
191 lines
6.8 KiB
Rust
// file: kb_rpc/src/endpoint_role.rs
|
|
// version: 2
|
|
|
|
//! Endpoint role helpers shared by HTTP and WebSocket pools.
|
|
|
|
/// Snapshot of one endpoint role and its local limits.
|
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct EndpointRoleSnapshot {
|
|
/// Role code used by endpoint pools.
|
|
pub role: std::string::String,
|
|
/// Enables this role on the endpoint.
|
|
pub enabled: bool,
|
|
/// Request or subscription kinds handled by this role.
|
|
pub request_kinds: std::vec::Vec<std::string::String>,
|
|
/// Role priority where lower values are preferred.
|
|
pub priority: u32,
|
|
/// Requests per second allowed for this role on this URL.
|
|
pub requests_per_second: u32,
|
|
/// Burst capacity allowed for this role on this URL.
|
|
pub burst_capacity: u32,
|
|
/// Maximum concurrent requests allowed for this role on this URL.
|
|
pub max_concurrent_requests: u32,
|
|
/// Maximum subscriptions allowed for this role on this URL.
|
|
pub max_subscriptions: u32,
|
|
/// Pause after a rate limit response in milliseconds.
|
|
pub pause_after_rate_limit_ms: u64,
|
|
}
|
|
|
|
impl crate::EndpointRoleSnapshot {
|
|
/// Builds a serializable snapshot from configuration.
|
|
pub fn from_config(config: &kb_config::EndpointRoleConfig) -> Self {
|
|
return Self {
|
|
role: config.role.clone(),
|
|
enabled: config.enabled,
|
|
request_kinds: config.request_kinds.clone(),
|
|
priority: config.priority,
|
|
requests_per_second: config.requests_per_second,
|
|
burst_capacity: config.burst_capacity,
|
|
max_concurrent_requests: config.max_concurrent_requests,
|
|
max_subscriptions: config.max_subscriptions,
|
|
pause_after_rate_limit_ms: config.pause_after_rate_limit_ms,
|
|
};
|
|
}
|
|
}
|
|
|
|
/// Converts a JSON-RPC method name into a stable snake_case request kind.
|
|
pub fn request_kind_from_method(method: &str) -> std::string::String {
|
|
let trimmed = method.trim();
|
|
if trimmed == "logsSubscribe" {
|
|
return "logs_subscribe_mentions".to_string();
|
|
}
|
|
return crate::endpoint_role::camel_or_pascal_to_snake(trimmed);
|
|
}
|
|
|
|
/// Returns true when one endpoint role can handle the requested role and kind.
|
|
pub(crate) fn role_matches(
|
|
role_config: &kb_config::EndpointRoleConfig,
|
|
required_role: &str,
|
|
request_kind: &str,
|
|
) -> bool {
|
|
if !role_config.enabled {
|
|
return false;
|
|
}
|
|
if role_config.role != required_role {
|
|
return false;
|
|
}
|
|
for configured_kind in &role_config.request_kinds {
|
|
if configured_kind == request_kind {
|
|
return true;
|
|
}
|
|
if configured_kind == "*" {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
fn camel_or_pascal_to_snake(value: &str) -> std::string::String {
|
|
let mut output = std::string::String::new();
|
|
let mut previous_was_lower_or_digit = false;
|
|
for character in value.chars() {
|
|
if character == '-' || character == ' ' || character == '.' {
|
|
if !output.ends_with('_') && !output.is_empty() {
|
|
output.push('_');
|
|
}
|
|
previous_was_lower_or_digit = false;
|
|
continue;
|
|
}
|
|
if character.is_ascii_uppercase() {
|
|
if previous_was_lower_or_digit && !output.ends_with('_') && !output.is_empty() {
|
|
output.push('_');
|
|
}
|
|
output.push(character.to_ascii_lowercase());
|
|
previous_was_lower_or_digit = false;
|
|
continue;
|
|
}
|
|
if character == '_' {
|
|
if !output.ends_with('_') && !output.is_empty() {
|
|
output.push('_');
|
|
}
|
|
previous_was_lower_or_digit = false;
|
|
continue;
|
|
}
|
|
output.push(character);
|
|
previous_was_lower_or_digit = character.is_ascii_lowercase() || character.is_ascii_digit();
|
|
}
|
|
return output.trim_matches('_').to_string();
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
fn role_config(
|
|
role: &str,
|
|
enabled: bool,
|
|
request_kinds: std::vec::Vec<std::string::String>,
|
|
) -> kb_config::EndpointRoleConfig {
|
|
return kb_config::EndpointRoleConfig {
|
|
role: role.to_string(),
|
|
enabled,
|
|
request_kinds,
|
|
priority: 1,
|
|
requests_per_second: 10,
|
|
burst_capacity: 10,
|
|
max_concurrent_requests: 4,
|
|
max_subscriptions: 16,
|
|
pause_after_rate_limit_ms: 1500,
|
|
};
|
|
}
|
|
|
|
#[test]
|
|
fn request_kind_converts_camel_case_methods() {
|
|
assert_eq!(crate::request_kind_from_method("getLatestBlockhash"), "get_latest_blockhash");
|
|
assert_eq!(crate::request_kind_from_method("sendTransaction"), "send_transaction");
|
|
}
|
|
|
|
#[test]
|
|
fn request_kind_keeps_existing_snake_case_methods() {
|
|
assert_eq!(crate::request_kind_from_method("get_latest_blockhash"), "get_latest_blockhash");
|
|
assert_eq!(
|
|
crate::request_kind_from_method("logs_subscribe_mentions"),
|
|
"logs_subscribe_mentions"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn request_kind_normalizes_separators() {
|
|
assert_eq!(crate::request_kind_from_method("get-Block"), "get_block");
|
|
assert_eq!(crate::request_kind_from_method("program.Subscribe"), "program_subscribe");
|
|
}
|
|
|
|
#[test]
|
|
fn request_kind_maps_logs_subscribe_to_mentions_role_kind() {
|
|
assert_eq!(crate::request_kind_from_method("logsSubscribe"), "logs_subscribe_mentions");
|
|
}
|
|
|
|
#[test]
|
|
fn role_matches_exact_request_kind() {
|
|
let role = role_config("http_queries", true, std::vec!["get_version".to_string()]);
|
|
assert!(crate::endpoint_role::role_matches(&role, "http_queries", "get_version"));
|
|
}
|
|
|
|
#[test]
|
|
fn role_matches_wildcard_request_kind() {
|
|
let role = role_config("http_queries", true, std::vec!["*".to_string()]);
|
|
assert!(crate::endpoint_role::role_matches(&role, "http_queries", "get_block"));
|
|
}
|
|
|
|
#[test]
|
|
fn role_does_not_match_when_disabled() {
|
|
let role = role_config("http_queries", false, std::vec!["*".to_string()]);
|
|
assert!(!crate::endpoint_role::role_matches(&role, "http_queries", "get_version"));
|
|
}
|
|
|
|
#[test]
|
|
fn role_does_not_match_different_role() {
|
|
let role = role_config("http_queries", true, std::vec!["*".to_string()]);
|
|
assert!(!crate::endpoint_role::role_matches(&role, "http_heavy", "get_block"));
|
|
}
|
|
|
|
#[test]
|
|
fn endpoint_role_snapshot_preserves_limits() {
|
|
let role = role_config("http_queries", true, std::vec!["get_version".to_string()]);
|
|
let snapshot = crate::EndpointRoleSnapshot::from_config(&role);
|
|
assert_eq!(snapshot.role, "http_queries");
|
|
assert_eq!(snapshot.request_kinds, std::vec!["get_version".to_string()]);
|
|
assert_eq!(snapshot.requests_per_second, 10);
|
|
assert_eq!(snapshot.max_subscriptions, 16);
|
|
}
|
|
}
|