v0.3.15-pre.006
This commit is contained in:
@@ -0,0 +1,375 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/route_inventory.rs
|
||||
// version: 1
|
||||
|
||||
//! Config-only logical route inventory for Raw Transaction Ingest Desk.
|
||||
|
||||
struct CompositeProfileCatalog {
|
||||
default_profile: String,
|
||||
profile_ids: std::vec::Vec<String>,
|
||||
}
|
||||
|
||||
/// Rebuilds the complete safe route inventory from validated Config without opening any network, Store or Worker resource.
|
||||
pub(crate) fn build_route_inventory(management: &ksp_config_lib::ConfigManagement, generation: u32) -> ksp_core_lib::Result<crate::RawIngestRouteInventoryDto> {
|
||||
let catalog = composite_profile_catalog(management);
|
||||
let catalog = match catalog {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let environment = ksp_config_lib::ConfigEnvironment::load();
|
||||
let environment = match environment {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut profiles = std::vec::Vec::with_capacity(catalog.profile_ids.len());
|
||||
for profile_id in &catalog.profile_ids {
|
||||
profiles.push(build_profile_inventory(management, &environment, profile_id.as_str(), profile_id == &catalog.default_profile));
|
||||
}
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_INVENTORY,
|
||||
generation,
|
||||
profile_count = profiles.len(),
|
||||
"rebuilt Raw Transaction Ingest Desk Config-only route inventory"
|
||||
);
|
||||
return std::result::Result::Ok(crate::RawIngestRouteInventoryDto { default_profile: catalog.default_profile, generation, profiles });
|
||||
}
|
||||
|
||||
fn composite_profile_catalog(management: &ksp_config_lib::ConfigManagement) -> ksp_core_lib::Result<CompositeProfileCatalog> {
|
||||
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_RAW_TRANSACTION_INGEST_DESK);
|
||||
let file_id = match file_id {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let document = management.engine().load_validated_document(&file_id);
|
||||
let document = match document {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let root = document.value().as_object();
|
||||
let root = match root {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return inventory_invalid("validated Raw Transaction Ingest Desk composite root is not an object"),
|
||||
};
|
||||
let default_profile = root.get("default_profile").and_then(|value| return value.as_str());
|
||||
let default_profile = match default_profile {
|
||||
std::option::Option::Some(value) if !value.is_empty() => value.to_owned(),
|
||||
_ => return inventory_invalid("validated Raw Transaction Ingest Desk composite has no readable default profile"),
|
||||
};
|
||||
let profile_values = root.get("profiles").and_then(|value| return value.as_array());
|
||||
let profile_values = match profile_values {
|
||||
std::option::Option::Some(value) if !value.is_empty() => value,
|
||||
_ => return inventory_invalid("validated Raw Transaction Ingest Desk composite has no profile inventory"),
|
||||
};
|
||||
let mut profile_ids = std::vec::Vec::with_capacity(profile_values.len());
|
||||
let mut seen = std::collections::BTreeSet::<String>::new();
|
||||
for profile in profile_values {
|
||||
let profile_id = profile.get("profile_id").and_then(|value| return value.as_str());
|
||||
let profile_id = match profile_id {
|
||||
std::option::Option::Some(value) if !value.is_empty() => value,
|
||||
_ => return inventory_invalid("validated Raw Transaction Ingest Desk composite contains an unreadable profile identifier"),
|
||||
};
|
||||
if !seen.insert(profile_id.to_owned()) {
|
||||
return inventory_invalid("validated Raw Transaction Ingest Desk composite contains duplicate profile identifiers");
|
||||
}
|
||||
profile_ids.push(profile_id.to_owned());
|
||||
}
|
||||
if !seen.contains(default_profile.as_str()) {
|
||||
return inventory_invalid("validated Raw Transaction Ingest Desk composite default profile is absent from the profile inventory");
|
||||
}
|
||||
return std::result::Result::Ok(CompositeProfileCatalog { default_profile, profile_ids });
|
||||
}
|
||||
|
||||
fn build_profile_inventory(
|
||||
management: &ksp_config_lib::ConfigManagement,
|
||||
environment: &ksp_config_lib::ConfigEnvironment,
|
||||
profile_id: &str,
|
||||
is_default: bool,
|
||||
) -> crate::RawIngestProfileInventoryDto {
|
||||
let composite = crate::load_raw_transaction_ingest_desk_composite_profile(management, std::option::Option::Some(profile_id));
|
||||
let composite = match composite {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return unavailable_profile(profile_id, is_default, std::option::Option::None, crate::RawIngestRouteUnavailableReason::ProfileUnresolved);
|
||||
},
|
||||
};
|
||||
let transport_profile =
|
||||
crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_TRANSPORT, ksp_config_lib::FILE_ID_STD_TRANSPORT);
|
||||
let transport_profile = match transport_profile {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return unavailable_profile(profile_id, is_default, std::option::Option::None, crate::RawIngestRouteUnavailableReason::ProfileUnresolved);
|
||||
},
|
||||
};
|
||||
let store_profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_STORE, ksp_config_lib::FILE_ID_STD_STORE);
|
||||
let store_profile = match store_profile {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return unavailable_profile(profile_id, is_default, std::option::Option::None, crate::RawIngestRouteUnavailableReason::ProfileUnresolved);
|
||||
},
|
||||
};
|
||||
let transport = management.engine().resolve_transport_config_profile(&transport_profile, environment);
|
||||
let store = management.engine().resolve_store_config_profile(&store_profile, environment);
|
||||
let network_hint = resolved_network_hint(&transport, &store);
|
||||
if transport.is_err() || store.is_err() {
|
||||
let reason = resolution_failure_reason(&transport, &store);
|
||||
return unavailable_profile(profile_id, is_default, network_hint.as_deref(), reason);
|
||||
}
|
||||
let transport = match transport {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return unavailable_profile(profile_id, is_default, network_hint.as_deref(), crate::RawIngestRouteUnavailableReason::ProfileUnresolved);
|
||||
},
|
||||
};
|
||||
let store = match store {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return unavailable_profile(profile_id, is_default, network_hint.as_deref(), crate::RawIngestRouteUnavailableReason::ProfileUnresolved);
|
||||
},
|
||||
};
|
||||
let store_network = store.settings().network().as_str().to_owned();
|
||||
if !transport_matches_network(&transport, store_network.as_str()) {
|
||||
return unavailable_profile(
|
||||
profile_id,
|
||||
is_default,
|
||||
std::option::Option::Some(store_network.as_str()),
|
||||
crate::RawIngestRouteUnavailableReason::NetworkMismatch,
|
||||
);
|
||||
}
|
||||
let routes = build_configured_profile_routes(&transport, store_network.as_str());
|
||||
return crate::RawIngestProfileInventoryDto {
|
||||
is_default,
|
||||
network: std::option::Option::Some(store_network),
|
||||
profile_id: profile_id.to_owned(),
|
||||
routes,
|
||||
};
|
||||
}
|
||||
|
||||
fn build_configured_profile_routes(transport: &ksp_config_lib::ResolvedTransportConfig, network: &str) -> std::vec::Vec<crate::RawIngestRouteDto> {
|
||||
let has_get_transaction = http_role_supports_methods(transport.http_settings(), &["getTransaction"]);
|
||||
let has_http_block_scan = http_role_supports_methods(transport.http_settings(), &["getSlot", "getBlocksWithLimit", "getBlock"]);
|
||||
let has_yellowstone = has_yellowstone_grpc(transport, network);
|
||||
let has_standard_logs =
|
||||
has_ws_capability(transport, network, ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard, ksp_onchain_transport_lib::WsSubscriptionKind::Logs);
|
||||
let has_standard_block =
|
||||
has_ws_capability(transport, network, ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard, ksp_onchain_transport_lib::WsSubscriptionKind::Block);
|
||||
let has_helius_transaction = has_ws_capability(
|
||||
transport,
|
||||
network,
|
||||
ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream,
|
||||
ksp_onchain_transport_lib::WsSubscriptionKind::HeliusTransaction,
|
||||
);
|
||||
return vec![
|
||||
route_with_requirements(
|
||||
crate::RawIngestRouteId::YellowstoneHydrated,
|
||||
network,
|
||||
&[
|
||||
(has_yellowstone, crate::RawIngestRouteUnavailableReason::MissingYellowstoneGrpc),
|
||||
(has_get_transaction, crate::RawIngestRouteUnavailableReason::MissingHttpGetTransaction),
|
||||
],
|
||||
),
|
||||
route_with_requirements(
|
||||
crate::RawIngestRouteId::StandardLogsHydrated,
|
||||
network,
|
||||
&[
|
||||
(has_standard_logs, crate::RawIngestRouteUnavailableReason::MissingWsLogsCapability),
|
||||
(has_get_transaction, crate::RawIngestRouteUnavailableReason::MissingHttpGetTransaction),
|
||||
],
|
||||
),
|
||||
route_with_requirements(
|
||||
crate::RawIngestRouteId::StandardBlockDirect,
|
||||
network,
|
||||
&[(has_standard_block, crate::RawIngestRouteUnavailableReason::MissingWsBlockCapability)],
|
||||
),
|
||||
route_with_requirements(
|
||||
crate::RawIngestRouteId::HeliusTransactionHydrated,
|
||||
network,
|
||||
&[
|
||||
(has_helius_transaction, crate::RawIngestRouteUnavailableReason::MissingHeliusTransactionCapability),
|
||||
(has_get_transaction, crate::RawIngestRouteUnavailableReason::MissingHttpGetTransaction),
|
||||
],
|
||||
),
|
||||
route_with_requirements(
|
||||
crate::RawIngestRouteId::HttpBlockPolling,
|
||||
network,
|
||||
&[(has_http_block_scan, crate::RawIngestRouteUnavailableReason::MissingHttpBlockScan)],
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
fn route_with_requirements(
|
||||
route_id: crate::RawIngestRouteId,
|
||||
network: &str,
|
||||
requirements: &[(bool, crate::RawIngestRouteUnavailableReason)],
|
||||
) -> crate::RawIngestRouteDto {
|
||||
for (available, reason) in requirements.iter().copied() {
|
||||
if !available {
|
||||
return crate::RawIngestRouteDto::unavailable(route_id, std::option::Option::Some(network), reason);
|
||||
}
|
||||
}
|
||||
return crate::RawIngestRouteDto::configured(route_id, network);
|
||||
}
|
||||
|
||||
fn unavailable_profile(
|
||||
profile_id: &str,
|
||||
is_default: bool,
|
||||
network: std::option::Option<&str>,
|
||||
reason: crate::RawIngestRouteUnavailableReason,
|
||||
) -> crate::RawIngestProfileInventoryDto {
|
||||
let routes = crate::RawIngestRouteId::all()
|
||||
.into_iter()
|
||||
.map(|route_id| return crate::RawIngestRouteDto::unavailable(route_id, network, reason))
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
return crate::RawIngestProfileInventoryDto { is_default, network: network.map(str::to_owned), profile_id: profile_id.to_owned(), routes };
|
||||
}
|
||||
|
||||
fn resolution_failure_reason(
|
||||
transport: &ksp_core_lib::Result<ksp_config_lib::ResolvedTransportConfig>,
|
||||
store: &ksp_core_lib::Result<ksp_config_lib::ResolvedStoreConfig>,
|
||||
) -> crate::RawIngestRouteUnavailableReason {
|
||||
let transport_missing_secret = match transport {
|
||||
std::result::Result::Err(error) => error.code() == ksp_config_lib::ERROR_CODE_ENVIRONMENT_VARIABLE_MISSING,
|
||||
std::result::Result::Ok(_) => false,
|
||||
};
|
||||
let store_missing_secret = match store {
|
||||
std::result::Result::Err(error) => error.code() == ksp_config_lib::ERROR_CODE_ENVIRONMENT_VARIABLE_MISSING,
|
||||
std::result::Result::Ok(_) => false,
|
||||
};
|
||||
if transport_missing_secret || store_missing_secret {
|
||||
return crate::RawIngestRouteUnavailableReason::MissingRequiredSecret;
|
||||
}
|
||||
return crate::RawIngestRouteUnavailableReason::ProfileUnresolved;
|
||||
}
|
||||
|
||||
fn resolved_network_hint(
|
||||
transport: &ksp_core_lib::Result<ksp_config_lib::ResolvedTransportConfig>,
|
||||
store: &ksp_core_lib::Result<ksp_config_lib::ResolvedStoreConfig>,
|
||||
) -> std::option::Option<String> {
|
||||
if let std::result::Result::Ok(store) = store {
|
||||
return std::option::Option::Some(store.settings().network().as_str().to_owned());
|
||||
}
|
||||
if let std::result::Result::Ok(transport) = transport {
|
||||
let networks = enabled_transport_networks(transport);
|
||||
if networks.len() == 1 {
|
||||
return networks.into_iter().next();
|
||||
}
|
||||
}
|
||||
return std::option::Option::None;
|
||||
}
|
||||
|
||||
fn transport_matches_network(transport: &ksp_config_lib::ResolvedTransportConfig, network: &str) -> bool {
|
||||
let networks = enabled_transport_networks(transport);
|
||||
return networks.len() == 1 && networks.contains(network);
|
||||
}
|
||||
|
||||
fn enabled_transport_networks(transport: &ksp_config_lib::ResolvedTransportConfig) -> std::collections::BTreeSet<String> {
|
||||
let mut networks = std::collections::BTreeSet::<String>::new();
|
||||
for endpoint in transport.http_settings().endpoints() {
|
||||
if endpoint.enabled() {
|
||||
networks.insert(endpoint.cluster().as_str().to_owned());
|
||||
}
|
||||
}
|
||||
if let std::option::Option::Some(settings) = transport.ws_settings() {
|
||||
for endpoint in settings.endpoints() {
|
||||
if endpoint.enabled() {
|
||||
networks.insert(endpoint.cluster().as_str().to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let std::option::Option::Some(settings) = transport.grpc_settings() {
|
||||
for endpoint in settings.endpoints() {
|
||||
if endpoint.enabled() {
|
||||
networks.insert(endpoint.cluster().as_str().to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
return networks;
|
||||
}
|
||||
|
||||
fn has_ws_capability(
|
||||
transport: &ksp_config_lib::ResolvedTransportConfig,
|
||||
network: &str,
|
||||
protocol: ksp_onchain_transport_lib::WsProtocolKind,
|
||||
capability: ksp_onchain_transport_lib::WsSubscriptionKind,
|
||||
) -> bool {
|
||||
let settings = match transport.ws_settings() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return false,
|
||||
};
|
||||
for endpoint in settings.endpoints() {
|
||||
if endpoint.enabled() && endpoint.cluster().as_str() == network && endpoint.protocol() == protocol && endpoint.supports_subscription(capability) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn has_yellowstone_grpc(transport: &ksp_config_lib::ResolvedTransportConfig, network: &str) -> bool {
|
||||
let settings = match transport.grpc_settings() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return false,
|
||||
};
|
||||
return settings.endpoints().iter().any(|endpoint| return endpoint.enabled() && endpoint.cluster().as_str() == network);
|
||||
}
|
||||
|
||||
fn http_role_supports_methods(settings: &ksp_onchain_transport_lib::HttpTransportSettings, methods: &[&'static str]) -> bool {
|
||||
let mut request_kinds = std::vec::Vec::<&'static str>::with_capacity(methods.len());
|
||||
for method_name in methods {
|
||||
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name);
|
||||
let descriptor = match descriptor {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return false,
|
||||
};
|
||||
request_kinds.push(descriptor.request_kind());
|
||||
}
|
||||
let mut role_names = std::collections::BTreeSet::<String>::new();
|
||||
for endpoint in settings.endpoints() {
|
||||
if !endpoint.enabled() {
|
||||
continue;
|
||||
}
|
||||
for role in endpoint.roles() {
|
||||
if role.enabled() {
|
||||
role_names.insert(role.role().as_str().to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
for role_name in role_names {
|
||||
let mut complete = true;
|
||||
for request_kind in &request_kinds {
|
||||
if !http_role_supports_request_kind(settings, role_name.as_str(), request_kind) {
|
||||
complete = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if complete {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn http_role_supports_request_kind(settings: &ksp_onchain_transport_lib::HttpTransportSettings, role_name: &str, request_kind: &str) -> bool {
|
||||
for endpoint in settings.endpoints() {
|
||||
if !endpoint.enabled() {
|
||||
continue;
|
||||
}
|
||||
for role in endpoint.roles() {
|
||||
if !role.enabled() || role.role().as_str() != role_name {
|
||||
continue;
|
||||
}
|
||||
for capability in role.request_kinds() {
|
||||
if capability.is_wildcard() || capability.as_str() == request_kind {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn inventory_invalid(message: &'static str) -> ksp_core_lib::Result<CompositeProfileCatalog> {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_ROUTE_INVENTORY_INVALID, message));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/route_inventory.rs"]
|
||||
mod tests;
|
||||
Reference in New Issue
Block a user