feat(euclid): add dynamic routing in core flows (#6333)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Prajjwal Kumar
2024-10-25 16:38:38 +05:30
committed by GitHub
parent 90765bece1
commit ce732db9b2
11 changed files with 603 additions and 490 deletions

View File

@ -12,10 +12,16 @@ use api_models::routing as routing_types;
use common_utils::ext_traits::ValueExt;
use common_utils::{ext_traits::Encode, id_type, types::keymanager::KeyManagerState};
use diesel_models::configs;
#[cfg(feature = "v1")]
use diesel_models::routing_algorithm;
use error_stack::ResultExt;
#[cfg(feature = "dynamic_routing")]
use external_services::grpc_client::dynamic_routing::SuccessBasedDynamicRouting;
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
use external_services::grpc_client::dynamic_routing::SuccessBasedDynamicRouting;
#[cfg(feature = "v1")]
use hyperswitch_domain_models::api::ApplicationResponse;
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
use router_env::logger;
#[cfg(any(feature = "dynamic_routing", feature = "v1"))]
use router_env::{instrument, metrics::add_attributes, tracing};
use rustc_hash::FxHashSet;
use storage_impl::redis::cache;
@ -29,8 +35,10 @@ use crate::{
types::{domain, storage},
utils::StringExt,
};
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
use crate::{core::metrics as core_metrics, routes::metrics};
#[cfg(feature = "v1")]
use crate::{core::metrics as core_metrics, routes::metrics, types::transformers::ForeignInto};
pub const SUCCESS_BASED_DYNAMIC_ROUTING_ALGORITHM: &str =
"Success rate based dynamic routing algorithm";
/// Provides us with all the configured configs of the Merchant in the ascending time configured
/// manner and chooses the first of them
@ -594,28 +602,8 @@ pub async fn refresh_success_based_routing_cache(
pub async fn fetch_success_based_routing_configs(
state: &SessionState,
business_profile: &domain::Profile,
dynamic_routing_algorithm: serde_json::Value,
success_based_routing_id: id_type::RoutingId,
) -> RouterResult<routing_types::SuccessBasedRoutingConfig> {
let dynamic_routing_algorithm_ref = dynamic_routing_algorithm
.parse_value::<routing_types::DynamicRoutingAlgorithmRef>("DynamicRoutingAlgorithmRef")
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("unable to parse dynamic_routing_algorithm_ref")?;
let success_based_routing_id = dynamic_routing_algorithm_ref
.success_based_algorithm
.ok_or(errors::ApiErrorResponse::GenericNotFoundError {
message: "success_based_algorithm not found in dynamic_routing_algorithm_ref"
.to_string(),
})?
.algorithm_id
// error can be possible when the feature is toggled off.
.ok_or(errors::ApiErrorResponse::GenericNotFoundError {
message: format!(
"unable to find algorithm_id in success based algorithm config as the feature is disabled for profile_id: {}",
business_profile.get_id().get_string_repr()
),
})?;
let key = format!(
"{}_{}",
business_profile.get_id().get_string_repr(),
@ -657,156 +645,185 @@ pub async fn push_metrics_for_success_based_routing(
payment_attempt: &storage::PaymentAttempt,
routable_connectors: Vec<routing_types::RoutableConnectorChoice>,
business_profile: &domain::Profile,
dynamic_routing_algorithm: serde_json::Value,
) -> RouterResult<()> {
let client = state
.grpc_client
.dynamic_routing
.success_rate_client
.as_ref()
.ok_or(errors::ApiErrorResponse::GenericNotFoundError {
message: "success_rate gRPC client not found".to_string(),
})?;
let success_based_dynamic_routing_algo_ref: routing_types::DynamicRoutingAlgorithmRef =
business_profile
.dynamic_routing_algorithm
.clone()
.map(|val| val.parse_value("DynamicRoutingAlgorithmRef"))
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to deserialize DynamicRoutingAlgorithmRef from JSON")?
.unwrap_or_default();
let payment_connector = &payment_attempt.connector.clone().ok_or(
errors::ApiErrorResponse::GenericNotFoundError {
message: "unable to derive payment connector from payment attempt".to_string(),
},
)?;
let success_based_algo_ref = success_based_dynamic_routing_algo_ref
.success_based_algorithm
.ok_or(errors::ApiErrorResponse::InternalServerError)
.attach_printable("success_based_algorithm not found in dynamic_routing_algorithm from business_profile table")?;
let success_based_routing_configs =
fetch_success_based_routing_configs(state, business_profile, dynamic_routing_algorithm)
if success_based_algo_ref.enabled_feature != routing_types::SuccessBasedRoutingFeatures::None {
let client = state
.grpc_client
.dynamic_routing
.success_rate_client
.as_ref()
.ok_or(errors::ApiErrorResponse::GenericNotFoundError {
message: "success_rate gRPC client not found".to_string(),
})?;
let payment_connector = &payment_attempt.connector.clone().ok_or(
errors::ApiErrorResponse::GenericNotFoundError {
message: "unable to derive payment connector from payment attempt".to_string(),
},
)?;
let success_based_routing_configs = fetch_success_based_routing_configs(
state,
business_profile,
success_based_algo_ref
.algorithm_id_with_timestamp
.algorithm_id
.ok_or(errors::ApiErrorResponse::InternalServerError)
.attach_printable(
"success_based_routing_algorithm_id not found in business_profile",
)?,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("unable to retrieve success_rate based dynamic routing configs")?;
let tenant_business_profile_id = generate_tenant_business_profile_id(
&state.tenant.redis_key_prefix,
business_profile.get_id().get_string_repr(),
);
let success_based_connectors = client
.calculate_success_rate(
tenant_business_profile_id.clone(),
success_based_routing_configs.clone(),
routable_connectors.clone(),
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("unable to retrieve success_rate based dynamic routing configs")?;
.attach_printable(
"unable to calculate/fetch success rate from dynamic routing service",
)?;
let tenant_business_profile_id = format!(
"{}:{}",
state.tenant.redis_key_prefix,
business_profile.get_id().get_string_repr()
);
let payment_status_attribute =
get_desired_payment_status_for_success_routing_metrics(&payment_attempt.status);
let success_based_connectors = client
.calculate_success_rate(
tenant_business_profile_id.clone(),
success_based_routing_configs.clone(),
routable_connectors.clone(),
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("unable to calculate/fetch success rate from dynamic routing service")?;
let first_success_based_connector_label = &success_based_connectors
.labels_with_score
.first()
.ok_or(errors::ApiErrorResponse::InternalServerError)
.attach_printable(
"unable to fetch the first connector from list of connectors obtained from dynamic routing service",
)?
.label
.to_string();
let payment_status_attribute =
get_desired_payment_status_for_success_routing_metrics(&payment_attempt.status);
let (first_success_based_connector, merchant_connector_id) = first_success_based_connector_label
.split_once(':')
.ok_or(errors::ApiErrorResponse::InternalServerError)
.attach_printable(
"unable to split connector_name and mca_id from the first connector obtained from dynamic routing service",
)?;
let first_success_based_connector_label = &success_based_connectors
.labels_with_score
.first()
.ok_or(errors::ApiErrorResponse::InternalServerError)
.attach_printable(
"unable to fetch the first connector from list of connectors obtained from dynamic routing service",
)?
.label
.to_string();
let outcome = get_success_based_metrics_outcome_for_payment(
&payment_status_attribute,
payment_connector.to_string(),
first_success_based_connector.to_string(),
);
let (first_success_based_connector, merchant_connector_id) = first_success_based_connector_label
.split_once(':')
.ok_or(errors::ApiErrorResponse::InternalServerError)
.attach_printable(
"unable to split connector_name and mca_id from the first connector obtained from dynamic routing service",
)?;
let outcome = get_success_based_metrics_outcome_for_payment(
&payment_status_attribute,
payment_connector.to_string(),
first_success_based_connector.to_string(),
);
core_metrics::DYNAMIC_SUCCESS_BASED_ROUTING.add(
&metrics::CONTEXT,
1,
&add_attributes([
("tenant", state.tenant.name.clone()),
(
"merchant_id",
payment_attempt.merchant_id.get_string_repr().to_string(),
),
(
"profile_id",
payment_attempt.profile_id.get_string_repr().to_string(),
),
("merchant_connector_id", merchant_connector_id.to_string()),
(
"payment_id",
payment_attempt.payment_id.get_string_repr().to_string(),
),
(
"success_based_routing_connector",
first_success_based_connector.to_string(),
),
("payment_connector", payment_connector.to_string()),
(
"currency",
payment_attempt
.currency
.map_or_else(|| "None".to_string(), |currency| currency.to_string()),
),
(
"payment_method",
payment_attempt.payment_method.map_or_else(
|| "None".to_string(),
|payment_method| payment_method.to_string(),
core_metrics::DYNAMIC_SUCCESS_BASED_ROUTING.add(
&metrics::CONTEXT,
1,
&add_attributes([
("tenant", state.tenant.name.clone()),
(
"merchant_id",
payment_attempt.merchant_id.get_string_repr().to_string(),
),
),
(
"payment_method_type",
payment_attempt.payment_method_type.map_or_else(
|| "None".to_string(),
|payment_method_type| payment_method_type.to_string(),
(
"profile_id",
payment_attempt.profile_id.get_string_repr().to_string(),
),
),
(
"capture_method",
payment_attempt.capture_method.map_or_else(
|| "None".to_string(),
|capture_method| capture_method.to_string(),
("merchant_connector_id", merchant_connector_id.to_string()),
(
"payment_id",
payment_attempt.payment_id.get_string_repr().to_string(),
),
),
(
"authentication_type",
payment_attempt.authentication_type.map_or_else(
|| "None".to_string(),
|authentication_type| authentication_type.to_string(),
(
"success_based_routing_connector",
first_success_based_connector.to_string(),
),
),
("payment_status", payment_attempt.status.to_string()),
("conclusive_classification", outcome.to_string()),
]),
);
("payment_connector", payment_connector.to_string()),
(
"currency",
payment_attempt
.currency
.map_or_else(|| "None".to_string(), |currency| currency.to_string()),
),
(
"payment_method",
payment_attempt.payment_method.map_or_else(
|| "None".to_string(),
|payment_method| payment_method.to_string(),
),
),
(
"payment_method_type",
payment_attempt.payment_method_type.map_or_else(
|| "None".to_string(),
|payment_method_type| payment_method_type.to_string(),
),
),
(
"capture_method",
payment_attempt.capture_method.map_or_else(
|| "None".to_string(),
|capture_method| capture_method.to_string(),
),
),
(
"authentication_type",
payment_attempt.authentication_type.map_or_else(
|| "None".to_string(),
|authentication_type| authentication_type.to_string(),
),
),
("payment_status", payment_attempt.status.to_string()),
("conclusive_classification", outcome.to_string()),
]),
);
logger::debug!("successfully pushed success_based_routing metrics");
client
.update_success_rate(
tenant_business_profile_id,
success_based_routing_configs,
vec![routing_types::RoutableConnectorChoiceWithStatus::new(
routing_types::RoutableConnectorChoice {
choice_kind: api_models::routing::RoutableChoiceKind::FullStruct,
connector: common_enums::RoutableConnectors::from_str(
payment_connector.as_str(),
)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("unable to infer routable_connector from connector")?,
merchant_connector_id: payment_attempt.merchant_connector_id.clone(),
},
payment_status_attribute == common_enums::AttemptStatus::Charged,
)],
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable(
"unable to update success based routing window in dynamic routing service",
)?;
Ok(())
client
.update_success_rate(
tenant_business_profile_id,
success_based_routing_configs,
vec![routing_types::RoutableConnectorChoiceWithStatus::new(
routing_types::RoutableConnectorChoice {
choice_kind: api_models::routing::RoutableChoiceKind::FullStruct,
connector: common_enums::RoutableConnectors::from_str(
payment_connector.as_str(),
)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("unable to infer routable_connector from connector")?,
merchant_connector_id: payment_attempt.merchant_connector_id.clone(),
},
payment_status_attribute == common_enums::AttemptStatus::Charged,
)],
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable(
"unable to update success based routing window in dynamic routing service",
)?;
Ok(())
} else {
Ok(())
}
}
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
@ -875,3 +892,67 @@ fn get_success_based_metrics_outcome_for_payment(
_ => common_enums::SuccessBasedRoutingConclusiveState::NonDeterministic,
}
}
/// generates cache key with tenant's redis key prefix and profile_id
pub fn generate_tenant_business_profile_id(
redis_key_prefix: &str,
business_profile_id: &str,
) -> String {
format!("{}:{}", redis_key_prefix, business_profile_id)
}
/// default config setup for success_based_routing
#[cfg(feature = "v1")]
#[instrument(skip_all)]
pub async fn default_success_based_routing_setup(
state: &SessionState,
key_store: domain::MerchantKeyStore,
business_profile: domain::Profile,
feature_to_enable: routing_types::SuccessBasedRoutingFeatures,
merchant_id: id_type::MerchantId,
mut success_based_dynamic_routing_algo: routing_types::DynamicRoutingAlgorithmRef,
) -> RouterResult<ApplicationResponse<routing_types::RoutingDictionaryRecord>> {
let db = state.store.as_ref();
let key_manager_state = &state.into();
let profile_id = business_profile.get_id().to_owned();
let default_success_based_routing_config = routing_types::SuccessBasedRoutingConfig::default();
let algorithm_id = common_utils::generate_routing_id_of_default_length();
let timestamp = common_utils::date_time::now();
let algo = routing_algorithm::RoutingAlgorithm {
algorithm_id: algorithm_id.clone(),
profile_id: profile_id.clone(),
merchant_id,
name: SUCCESS_BASED_DYNAMIC_ROUTING_ALGORITHM.to_string(),
description: None,
kind: diesel_models::enums::RoutingAlgorithmKind::Dynamic,
algorithm_data: serde_json::json!(default_success_based_routing_config),
created_at: timestamp,
modified_at: timestamp,
algorithm_for: common_enums::TransactionType::Payment,
};
let record = db
.insert_routing_algorithm(algo)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Unable to insert record in routing algorithm table")?;
success_based_dynamic_routing_algo.update_algorithm_id(algorithm_id, feature_to_enable);
update_business_profile_active_dynamic_algorithm_ref(
db,
key_manager_state,
&key_store,
business_profile,
success_based_dynamic_routing_algo,
)
.await?;
let new_record = record.foreign_into();
core_metrics::ROUTING_CREATE_SUCCESS_RESPONSE.add(
&metrics::CONTEXT,
1,
&add_attributes([("profile_id", profile_id.get_string_repr().to_string())]),
);
Ok(ApplicationResponse::Json(new_record))
}