feat(routing): Integrate global success rates (#6950)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Sarthak Soni
2025-01-21 17:14:14 +05:30
committed by GitHub
parent 100a1783ac
commit 39d2d6c438
11 changed files with 196 additions and 17 deletions

View File

@ -25202,9 +25202,19 @@
} }
], ],
"nullable": true "nullable": true
},
"specificity_level": {
"$ref": "#/components/schemas/SuccessRateSpecificityLevel"
} }
} }
}, },
"SuccessRateSpecificityLevel": {
"type": "string",
"enum": [
"merchant",
"global"
]
},
"SupportedPaymentMethod": { "SupportedPaymentMethod": {
"allOf": [ "allOf": [
{ {

View File

@ -779,6 +779,7 @@ impl Default for SuccessBasedRoutingConfig {
duration_in_mins: Some(5), duration_in_mins: Some(5),
max_total_count: Some(2), max_total_count: Some(2),
}), }),
specificity_level: SuccessRateSpecificityLevel::default(),
}), }),
} }
} }
@ -801,6 +802,8 @@ pub struct SuccessBasedRoutingConfigBody {
pub default_success_rate: Option<f64>, pub default_success_rate: Option<f64>,
pub max_aggregates_size: Option<u32>, pub max_aggregates_size: Option<u32>,
pub current_block_threshold: Option<CurrentBlockThreshold>, pub current_block_threshold: Option<CurrentBlockThreshold>,
#[serde(default)]
pub specificity_level: SuccessRateSpecificityLevel,
} }
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, ToSchema)] #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, ToSchema)]
@ -809,6 +812,14 @@ pub struct CurrentBlockThreshold {
pub max_total_count: Option<u64>, pub max_total_count: Option<u64>,
} }
#[derive(serde::Serialize, serde::Deserialize, Debug, Default, Clone, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum SuccessRateSpecificityLevel {
#[default]
Merchant,
Global,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SuccessBasedRoutingPayloadWrapper { pub struct SuccessBasedRoutingPayloadWrapper {
pub updated_config: SuccessBasedRoutingConfig, pub updated_config: SuccessBasedRoutingConfig,
@ -849,6 +860,7 @@ impl SuccessBasedRoutingConfigBody {
.as_mut() .as_mut()
.map(|threshold| threshold.update(current_block_threshold)); .map(|threshold| threshold.update(current_block_threshold));
} }
self.specificity_level = new.specificity_level
} }
} }

View File

@ -20,6 +20,7 @@ pub struct DynamicRoutingStatsNew {
pub conclusive_classification: common_enums::SuccessBasedRoutingConclusiveState, pub conclusive_classification: common_enums::SuccessBasedRoutingConclusiveState,
pub created_at: time::PrimitiveDateTime, pub created_at: time::PrimitiveDateTime,
pub payment_method_type: Option<common_enums::PaymentMethodType>, pub payment_method_type: Option<common_enums::PaymentMethodType>,
pub global_success_based_connector: Option<String>,
} }
#[derive(Clone, Debug, Eq, PartialEq, Queryable, Selectable, Insertable)] #[derive(Clone, Debug, Eq, PartialEq, Queryable, Selectable, Insertable)]
@ -40,4 +41,5 @@ pub struct DynamicRoutingStats {
pub conclusive_classification: common_enums::SuccessBasedRoutingConclusiveState, pub conclusive_classification: common_enums::SuccessBasedRoutingConclusiveState,
pub created_at: time::PrimitiveDateTime, pub created_at: time::PrimitiveDateTime,
pub payment_method_type: Option<common_enums::PaymentMethodType>, pub payment_method_type: Option<common_enums::PaymentMethodType>,
pub global_success_based_connector: Option<String>,
} }

View File

@ -435,6 +435,8 @@ diesel::table! {
created_at -> Timestamp, created_at -> Timestamp,
#[max_length = 64] #[max_length = 64]
payment_method_type -> Nullable<Varchar>, payment_method_type -> Nullable<Varchar>,
#[max_length = 64]
global_success_based_connector -> Nullable<Varchar>,
} }
} }

View File

@ -447,6 +447,8 @@ diesel::table! {
created_at -> Timestamp, created_at -> Timestamp,
#[max_length = 64] #[max_length = 64]
payment_method_type -> Nullable<Varchar>, payment_method_type -> Nullable<Varchar>,
#[max_length = 64]
global_success_based_connector -> Nullable<Varchar>,
} }
} }

View File

@ -1,15 +1,17 @@
use api_models::routing::{ use api_models::routing::{
CurrentBlockThreshold, RoutableConnectorChoice, RoutableConnectorChoiceWithStatus, CurrentBlockThreshold, RoutableConnectorChoice, RoutableConnectorChoiceWithStatus,
SuccessBasedRoutingConfig, SuccessBasedRoutingConfigBody, SuccessBasedRoutingConfig, SuccessBasedRoutingConfigBody, SuccessRateSpecificityLevel,
}; };
use common_utils::{ext_traits::OptionExt, transformers::ForeignTryFrom}; use common_utils::{ext_traits::OptionExt, transformers::ForeignTryFrom};
use error_stack::ResultExt; use error_stack::ResultExt;
use router_env::{instrument, logger, tracing}; use router_env::{instrument, logger, tracing};
pub use success_rate::{ pub use success_rate::{
success_rate_calculator_client::SuccessRateCalculatorClient, CalSuccessRateConfig, success_rate_calculator_client::SuccessRateCalculatorClient, CalGlobalSuccessRateConfig,
CalGlobalSuccessRateRequest, CalGlobalSuccessRateResponse, CalSuccessRateConfig,
CalSuccessRateRequest, CalSuccessRateResponse, CalSuccessRateRequest, CalSuccessRateResponse,
CurrentBlockThreshold as DynamicCurrentThreshold, InvalidateWindowsRequest, CurrentBlockThreshold as DynamicCurrentThreshold, InvalidateWindowsRequest,
InvalidateWindowsResponse, LabelWithStatus, UpdateSuccessRateWindowConfig, InvalidateWindowsResponse, LabelWithStatus,
SuccessRateSpecificityLevel as ProtoSpecificityLevel, UpdateSuccessRateWindowConfig,
UpdateSuccessRateWindowRequest, UpdateSuccessRateWindowResponse, UpdateSuccessRateWindowRequest, UpdateSuccessRateWindowResponse,
}; };
#[allow( #[allow(
@ -51,6 +53,15 @@ pub trait SuccessBasedDynamicRouting: dyn_clone::DynClone + Send + Sync {
id: String, id: String,
headers: GrpcHeaders, headers: GrpcHeaders,
) -> DynamicRoutingResult<InvalidateWindowsResponse>; ) -> DynamicRoutingResult<InvalidateWindowsResponse>;
/// To calculate both global and merchant specific success rate for the list of chosen connectors
async fn calculate_entity_and_global_success_rate(
&self,
id: String,
success_rate_based_config: SuccessBasedRoutingConfig,
params: String,
label_input: Vec<RoutableConnectorChoice>,
headers: GrpcHeaders,
) -> DynamicRoutingResult<CalGlobalSuccessRateResponse>;
} }
#[async_trait::async_trait] #[async_trait::async_trait]
@ -113,6 +124,7 @@ impl SuccessBasedDynamicRouting for SuccessRateCalculatorClient<Client> {
.transpose()?; .transpose()?;
let labels_with_status = label_input let labels_with_status = label_input
.clone()
.into_iter() .into_iter()
.map(|conn_choice| LabelWithStatus { .map(|conn_choice| LabelWithStatus {
label: conn_choice.routable_connector_choice.to_string(), label: conn_choice.routable_connector_choice.to_string(),
@ -120,12 +132,21 @@ impl SuccessBasedDynamicRouting for SuccessRateCalculatorClient<Client> {
}) })
.collect(); .collect();
let global_labels_with_status = label_input
.into_iter()
.map(|conn_choice| LabelWithStatus {
label: conn_choice.routable_connector_choice.connector.to_string(),
status: conn_choice.status,
})
.collect();
let request = grpc_client::create_grpc_request( let request = grpc_client::create_grpc_request(
UpdateSuccessRateWindowRequest { UpdateSuccessRateWindowRequest {
id, id,
params, params,
labels_with_status, labels_with_status,
config, config,
global_labels_with_status,
}, },
headers, headers,
); );
@ -165,6 +186,55 @@ impl SuccessBasedDynamicRouting for SuccessRateCalculatorClient<Client> {
Ok(response) Ok(response)
} }
async fn calculate_entity_and_global_success_rate(
&self,
id: String,
success_rate_based_config: SuccessBasedRoutingConfig,
params: String,
label_input: Vec<RoutableConnectorChoice>,
headers: GrpcHeaders,
) -> DynamicRoutingResult<CalGlobalSuccessRateResponse> {
let labels = label_input
.clone()
.into_iter()
.map(|conn_choice| conn_choice.to_string())
.collect::<Vec<_>>();
let global_labels = label_input
.into_iter()
.map(|conn_choice| conn_choice.connector.to_string())
.collect::<Vec<_>>();
let config = success_rate_based_config
.config
.map(ForeignTryFrom::foreign_try_from)
.transpose()?;
let request = grpc_client::create_grpc_request(
CalGlobalSuccessRateRequest {
entity_id: id,
entity_params: params,
entity_labels: labels,
global_labels,
config,
},
headers,
);
let response = self
.clone()
.fetch_entity_and_global_success_rate(request)
.await
.change_context(DynamicRoutingError::SuccessRateBasedRoutingFailure(
"Failed to fetch the entity and global success rate".to_string(),
))?
.into_inner();
logger::info!(dynamic_routing_response=?response);
Ok(response)
}
} }
impl ForeignTryFrom<CurrentBlockThreshold> for DynamicCurrentThreshold { impl ForeignTryFrom<CurrentBlockThreshold> for DynamicCurrentThreshold {
@ -216,6 +286,30 @@ impl ForeignTryFrom<SuccessBasedRoutingConfigBody> for CalSuccessRateConfig {
.change_context(DynamicRoutingError::MissingRequiredField { .change_context(DynamicRoutingError::MissingRequiredField {
field: "default_success_rate".to_string(), field: "default_success_rate".to_string(),
})?, })?,
specificity_level: match config.specificity_level {
SuccessRateSpecificityLevel::Merchant => Some(ProtoSpecificityLevel::Entity.into()),
SuccessRateSpecificityLevel::Global => Some(ProtoSpecificityLevel::Global.into()),
},
})
}
}
impl ForeignTryFrom<SuccessBasedRoutingConfigBody> for CalGlobalSuccessRateConfig {
type Error = error_stack::Report<DynamicRoutingError>;
fn foreign_try_from(config: SuccessBasedRoutingConfigBody) -> Result<Self, Self::Error> {
Ok(Self {
entity_min_aggregates_size: config
.min_aggregates_size
.get_required_value("min_aggregate_size")
.change_context(DynamicRoutingError::MissingRequiredField {
field: "min_aggregates_size".to_string(),
})?,
entity_default_success_rate: config
.default_success_rate
.get_required_value("default_success_rate")
.change_context(DynamicRoutingError::MissingRequiredField {
field: "default_success_rate".to_string(),
})?,
}) })
} }
} }

View File

@ -624,6 +624,7 @@ Never share your secret api keys. Keep them guarded and secure.
api_models::routing::StraightThroughAlgorithm, api_models::routing::StraightThroughAlgorithm,
api_models::routing::ConnectorVolumeSplit, api_models::routing::ConnectorVolumeSplit,
api_models::routing::ConnectorSelection, api_models::routing::ConnectorSelection,
api_models::routing::SuccessRateSpecificityLevel,
api_models::routing::ToggleDynamicRoutingQuery, api_models::routing::ToggleDynamicRoutingQuery,
api_models::routing::ToggleDynamicRoutingPath, api_models::routing::ToggleDynamicRoutingPath,
api_models::routing::ast::RoutableChoiceKind, api_models::routing::ast::RoutableChoiceKind,

View File

@ -714,7 +714,7 @@ pub async fn push_metrics_with_update_window_for_success_based_routing(
); );
let success_based_connectors = client let success_based_connectors = client
.calculate_success_rate( .calculate_entity_and_global_success_rate(
business_profile.get_id().get_string_repr().into(), business_profile.get_id().get_string_repr().into(),
success_based_routing_configs.clone(), success_based_routing_configs.clone(),
success_based_routing_config_params.clone(), success_based_routing_config_params.clone(),
@ -730,28 +730,34 @@ pub async fn push_metrics_with_update_window_for_success_based_routing(
let payment_status_attribute = let payment_status_attribute =
get_desired_payment_status_for_success_routing_metrics(payment_attempt.status); get_desired_payment_status_for_success_routing_metrics(payment_attempt.status);
let first_success_based_connector_label = &success_based_connectors let first_merchant_success_based_connector = &success_based_connectors
.labels_with_score .entity_scores_with_labels
.first() .first()
.ok_or(errors::ApiErrorResponse::InternalServerError) .ok_or(errors::ApiErrorResponse::InternalServerError)
.attach_printable( .attach_printable(
"unable to fetch the first connector from list of connectors obtained from dynamic routing service", "unable to fetch the first connector from list of connectors obtained from dynamic routing service",
)? )?;
.label
.to_string();
let (first_success_based_connector, _) = first_success_based_connector_label let (first_merchant_success_based_connector_label, _) = first_merchant_success_based_connector.label
.split_once(':') .split_once(':')
.ok_or(errors::ApiErrorResponse::InternalServerError) .ok_or(errors::ApiErrorResponse::InternalServerError)
.attach_printable(format!( .attach_printable(format!(
"unable to split connector_name and mca_id from the first connector {:?} obtained from dynamic routing service", "unable to split connector_name and mca_id from the first connector {:?} obtained from dynamic routing service",
first_success_based_connector_label first_merchant_success_based_connector.label
))?; ))?;
let first_global_success_based_connector = &success_based_connectors
.global_scores_with_labels
.first()
.ok_or(errors::ApiErrorResponse::InternalServerError)
.attach_printable(
"unable to fetch the first global connector from list of connectors obtained from dynamic routing service",
)?;
let outcome = get_success_based_metrics_outcome_for_payment( let outcome = get_success_based_metrics_outcome_for_payment(
payment_status_attribute, payment_status_attribute,
payment_connector.to_string(), payment_connector.to_string(),
first_success_based_connector.to_string(), first_merchant_success_based_connector_label.to_string(),
); );
let dynamic_routing_stats = DynamicRoutingStatsNew { let dynamic_routing_stats = DynamicRoutingStatsNew {
@ -760,7 +766,8 @@ pub async fn push_metrics_with_update_window_for_success_based_routing(
merchant_id: payment_attempt.merchant_id.to_owned(), merchant_id: payment_attempt.merchant_id.to_owned(),
profile_id: payment_attempt.profile_id.to_owned(), profile_id: payment_attempt.profile_id.to_owned(),
amount: payment_attempt.get_total_amount(), amount: payment_attempt.get_total_amount(),
success_based_routing_connector: first_success_based_connector.to_string(), success_based_routing_connector: first_merchant_success_based_connector_label
.to_string(),
payment_connector: payment_connector.to_string(), payment_connector: payment_connector.to_string(),
payment_method_type: payment_attempt.payment_method_type, payment_method_type: payment_attempt.payment_method_type,
currency: payment_attempt.currency, currency: payment_attempt.currency,
@ -770,6 +777,9 @@ pub async fn push_metrics_with_update_window_for_success_based_routing(
payment_status: payment_attempt.status, payment_status: payment_attempt.status,
conclusive_classification: outcome, conclusive_classification: outcome,
created_at: common_utils::date_time::now(), created_at: common_utils::date_time::now(),
global_success_based_connector: Some(
first_global_success_based_connector.label.to_string(),
),
}; };
core_metrics::DYNAMIC_SUCCESS_BASED_ROUTING.add( core_metrics::DYNAMIC_SUCCESS_BASED_ROUTING.add(
@ -788,8 +798,20 @@ pub async fn push_metrics_with_update_window_for_success_based_routing(
), ),
), ),
( (
"success_based_routing_connector", "merchant_specific_success_based_routing_connector",
first_success_based_connector.to_string(), first_merchant_success_based_connector_label.to_string(),
),
(
"merchant_specific_success_based_routing_connector_score",
first_merchant_success_based_connector.score.to_string(),
),
(
"global_success_based_routing_connector",
first_global_success_based_connector.label.to_string(),
),
(
"global_success_based_routing_connector_score",
first_global_success_based_connector.score.to_string(),
), ),
("payment_connector", payment_connector.to_string()), ("payment_connector", payment_connector.to_string()),
( (

View File

@ -0,0 +1,3 @@
-- This file should undo anything in `up.sql`
ALTER TABLE dynamic_routing_stats
DROP COLUMN IF EXISTS global_success_based_connector;

View File

@ -0,0 +1,3 @@
-- Your SQL goes here
ALTER TABLE dynamic_routing_stats
ADD COLUMN IF NOT EXISTS global_success_based_connector VARCHAR(64);

View File

@ -7,6 +7,8 @@ service SuccessRateCalculator {
rpc UpdateSuccessRateWindow (UpdateSuccessRateWindowRequest) returns (UpdateSuccessRateWindowResponse); rpc UpdateSuccessRateWindow (UpdateSuccessRateWindowRequest) returns (UpdateSuccessRateWindowResponse);
rpc InvalidateWindows (InvalidateWindowsRequest) returns (InvalidateWindowsResponse); rpc InvalidateWindows (InvalidateWindowsRequest) returns (InvalidateWindowsResponse);
rpc FetchEntityAndGlobalSuccessRate (CalGlobalSuccessRateRequest) returns (CalGlobalSuccessRateResponse);
} }
// API-1 types // API-1 types
@ -20,6 +22,12 @@ message CalSuccessRateRequest {
message CalSuccessRateConfig { message CalSuccessRateConfig {
uint32 min_aggregates_size = 1; uint32 min_aggregates_size = 1;
double default_success_rate = 2; double default_success_rate = 2;
optional SuccessRateSpecificityLevel specificity_level = 3;
}
enum SuccessRateSpecificityLevel {
ENTITY = 0;
GLOBAL = 1;
} }
message CalSuccessRateResponse { message CalSuccessRateResponse {
@ -31,12 +39,13 @@ message LabelWithScore {
string label = 2; string label = 2;
} }
// API-2 types // API-2 types
message UpdateSuccessRateWindowRequest { message UpdateSuccessRateWindowRequest {
string id = 1; string id = 1;
string params = 2; string params = 2;
repeated LabelWithStatus labels_with_status = 3; repeated LabelWithStatus labels_with_status = 3;
UpdateSuccessRateWindowConfig config = 4; UpdateSuccessRateWindowConfig config = 4;
repeated LabelWithStatus global_labels_with_status = 5;
} }
message LabelWithStatus { message LabelWithStatus {
@ -68,9 +77,28 @@ message InvalidateWindowsRequest {
} }
message InvalidateWindowsResponse { message InvalidateWindowsResponse {
enum InvalidationStatus { enum InvalidationStatus {
WINDOW_INVALIDATION_SUCCEEDED = 0; WINDOW_INVALIDATION_SUCCEEDED = 0;
WINDOW_INVALIDATION_FAILED = 1; WINDOW_INVALIDATION_FAILED = 1;
} }
InvalidationStatus status = 1; InvalidationStatus status = 1;
}
// API-4 types
message CalGlobalSuccessRateRequest {
string entity_id = 1;
string entity_params = 2;
repeated string entity_labels = 3;
repeated string global_labels = 4;
CalGlobalSuccessRateConfig config = 5;
}
message CalGlobalSuccessRateConfig {
uint32 entity_min_aggregates_size = 1;
double entity_default_success_rate = 2;
}
message CalGlobalSuccessRateResponse {
repeated LabelWithScore entity_scores_with_labels = 1;
repeated LabelWithScore global_scores_with_labels = 2;
} }