mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-01 11:06:50 +08:00
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:
@ -25202,9 +25202,19 @@
|
||||
}
|
||||
],
|
||||
"nullable": true
|
||||
},
|
||||
"specificity_level": {
|
||||
"$ref": "#/components/schemas/SuccessRateSpecificityLevel"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SuccessRateSpecificityLevel": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"merchant",
|
||||
"global"
|
||||
]
|
||||
},
|
||||
"SupportedPaymentMethod": {
|
||||
"allOf": [
|
||||
{
|
||||
|
||||
@ -779,6 +779,7 @@ impl Default for SuccessBasedRoutingConfig {
|
||||
duration_in_mins: Some(5),
|
||||
max_total_count: Some(2),
|
||||
}),
|
||||
specificity_level: SuccessRateSpecificityLevel::default(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
@ -801,6 +802,8 @@ pub struct SuccessBasedRoutingConfigBody {
|
||||
pub default_success_rate: Option<f64>,
|
||||
pub max_aggregates_size: Option<u32>,
|
||||
pub current_block_threshold: Option<CurrentBlockThreshold>,
|
||||
#[serde(default)]
|
||||
pub specificity_level: SuccessRateSpecificityLevel,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, ToSchema)]
|
||||
@ -809,6 +812,14 @@ pub struct CurrentBlockThreshold {
|
||||
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)]
|
||||
pub struct SuccessBasedRoutingPayloadWrapper {
|
||||
pub updated_config: SuccessBasedRoutingConfig,
|
||||
@ -849,6 +860,7 @@ impl SuccessBasedRoutingConfigBody {
|
||||
.as_mut()
|
||||
.map(|threshold| threshold.update(current_block_threshold));
|
||||
}
|
||||
self.specificity_level = new.specificity_level
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@ pub struct DynamicRoutingStatsNew {
|
||||
pub conclusive_classification: common_enums::SuccessBasedRoutingConclusiveState,
|
||||
pub created_at: time::PrimitiveDateTime,
|
||||
pub payment_method_type: Option<common_enums::PaymentMethodType>,
|
||||
pub global_success_based_connector: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Queryable, Selectable, Insertable)]
|
||||
@ -40,4 +41,5 @@ pub struct DynamicRoutingStats {
|
||||
pub conclusive_classification: common_enums::SuccessBasedRoutingConclusiveState,
|
||||
pub created_at: time::PrimitiveDateTime,
|
||||
pub payment_method_type: Option<common_enums::PaymentMethodType>,
|
||||
pub global_success_based_connector: Option<String>,
|
||||
}
|
||||
|
||||
@ -435,6 +435,8 @@ diesel::table! {
|
||||
created_at -> Timestamp,
|
||||
#[max_length = 64]
|
||||
payment_method_type -> Nullable<Varchar>,
|
||||
#[max_length = 64]
|
||||
global_success_based_connector -> Nullable<Varchar>,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -447,6 +447,8 @@ diesel::table! {
|
||||
created_at -> Timestamp,
|
||||
#[max_length = 64]
|
||||
payment_method_type -> Nullable<Varchar>,
|
||||
#[max_length = 64]
|
||||
global_success_based_connector -> Nullable<Varchar>,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,15 +1,17 @@
|
||||
use api_models::routing::{
|
||||
CurrentBlockThreshold, RoutableConnectorChoice, RoutableConnectorChoiceWithStatus,
|
||||
SuccessBasedRoutingConfig, SuccessBasedRoutingConfigBody,
|
||||
SuccessBasedRoutingConfig, SuccessBasedRoutingConfigBody, SuccessRateSpecificityLevel,
|
||||
};
|
||||
use common_utils::{ext_traits::OptionExt, transformers::ForeignTryFrom};
|
||||
use error_stack::ResultExt;
|
||||
use router_env::{instrument, logger, tracing};
|
||||
pub use success_rate::{
|
||||
success_rate_calculator_client::SuccessRateCalculatorClient, CalSuccessRateConfig,
|
||||
success_rate_calculator_client::SuccessRateCalculatorClient, CalGlobalSuccessRateConfig,
|
||||
CalGlobalSuccessRateRequest, CalGlobalSuccessRateResponse, CalSuccessRateConfig,
|
||||
CalSuccessRateRequest, CalSuccessRateResponse,
|
||||
CurrentBlockThreshold as DynamicCurrentThreshold, InvalidateWindowsRequest,
|
||||
InvalidateWindowsResponse, LabelWithStatus, UpdateSuccessRateWindowConfig,
|
||||
InvalidateWindowsResponse, LabelWithStatus,
|
||||
SuccessRateSpecificityLevel as ProtoSpecificityLevel, UpdateSuccessRateWindowConfig,
|
||||
UpdateSuccessRateWindowRequest, UpdateSuccessRateWindowResponse,
|
||||
};
|
||||
#[allow(
|
||||
@ -51,6 +53,15 @@ pub trait SuccessBasedDynamicRouting: dyn_clone::DynClone + Send + Sync {
|
||||
id: String,
|
||||
headers: GrpcHeaders,
|
||||
) -> 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]
|
||||
@ -113,6 +124,7 @@ impl SuccessBasedDynamicRouting for SuccessRateCalculatorClient<Client> {
|
||||
.transpose()?;
|
||||
|
||||
let labels_with_status = label_input
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|conn_choice| LabelWithStatus {
|
||||
label: conn_choice.routable_connector_choice.to_string(),
|
||||
@ -120,12 +132,21 @@ impl SuccessBasedDynamicRouting for SuccessRateCalculatorClient<Client> {
|
||||
})
|
||||
.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(
|
||||
UpdateSuccessRateWindowRequest {
|
||||
id,
|
||||
params,
|
||||
labels_with_status,
|
||||
config,
|
||||
global_labels_with_status,
|
||||
},
|
||||
headers,
|
||||
);
|
||||
@ -165,6 +186,55 @@ impl SuccessBasedDynamicRouting for SuccessRateCalculatorClient<Client> {
|
||||
|
||||
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 {
|
||||
@ -216,6 +286,30 @@ impl ForeignTryFrom<SuccessBasedRoutingConfigBody> for CalSuccessRateConfig {
|
||||
.change_context(DynamicRoutingError::MissingRequiredField {
|
||||
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(),
|
||||
})?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -624,6 +624,7 @@ Never share your secret api keys. Keep them guarded and secure.
|
||||
api_models::routing::StraightThroughAlgorithm,
|
||||
api_models::routing::ConnectorVolumeSplit,
|
||||
api_models::routing::ConnectorSelection,
|
||||
api_models::routing::SuccessRateSpecificityLevel,
|
||||
api_models::routing::ToggleDynamicRoutingQuery,
|
||||
api_models::routing::ToggleDynamicRoutingPath,
|
||||
api_models::routing::ast::RoutableChoiceKind,
|
||||
|
||||
@ -714,7 +714,7 @@ pub async fn push_metrics_with_update_window_for_success_based_routing(
|
||||
);
|
||||
|
||||
let success_based_connectors = client
|
||||
.calculate_success_rate(
|
||||
.calculate_entity_and_global_success_rate(
|
||||
business_profile.get_id().get_string_repr().into(),
|
||||
success_based_routing_configs.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 =
|
||||
get_desired_payment_status_for_success_routing_metrics(payment_attempt.status);
|
||||
|
||||
let first_success_based_connector_label = &success_based_connectors
|
||||
.labels_with_score
|
||||
let first_merchant_success_based_connector = &success_based_connectors
|
||||
.entity_scores_with_labels
|
||||
.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 (first_success_based_connector, _) = first_success_based_connector_label
|
||||
let (first_merchant_success_based_connector_label, _) = first_merchant_success_based_connector.label
|
||||
.split_once(':')
|
||||
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable(format!(
|
||||
"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(
|
||||
payment_status_attribute,
|
||||
payment_connector.to_string(),
|
||||
first_success_based_connector.to_string(),
|
||||
first_merchant_success_based_connector_label.to_string(),
|
||||
);
|
||||
|
||||
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(),
|
||||
profile_id: payment_attempt.profile_id.to_owned(),
|
||||
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_method_type: payment_attempt.payment_method_type,
|
||||
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,
|
||||
conclusive_classification: outcome,
|
||||
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(
|
||||
@ -788,8 +798,20 @@ pub async fn push_metrics_with_update_window_for_success_based_routing(
|
||||
),
|
||||
),
|
||||
(
|
||||
"success_based_routing_connector",
|
||||
first_success_based_connector.to_string(),
|
||||
"merchant_specific_success_based_routing_connector",
|
||||
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()),
|
||||
(
|
||||
|
||||
@ -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;
|
||||
@ -0,0 +1,3 @@
|
||||
-- Your SQL goes here
|
||||
ALTER TABLE dynamic_routing_stats
|
||||
ADD COLUMN IF NOT EXISTS global_success_based_connector VARCHAR(64);
|
||||
@ -7,6 +7,8 @@ service SuccessRateCalculator {
|
||||
rpc UpdateSuccessRateWindow (UpdateSuccessRateWindowRequest) returns (UpdateSuccessRateWindowResponse);
|
||||
|
||||
rpc InvalidateWindows (InvalidateWindowsRequest) returns (InvalidateWindowsResponse);
|
||||
|
||||
rpc FetchEntityAndGlobalSuccessRate (CalGlobalSuccessRateRequest) returns (CalGlobalSuccessRateResponse);
|
||||
}
|
||||
|
||||
// API-1 types
|
||||
@ -20,6 +22,12 @@ message CalSuccessRateRequest {
|
||||
message CalSuccessRateConfig {
|
||||
uint32 min_aggregates_size = 1;
|
||||
double default_success_rate = 2;
|
||||
optional SuccessRateSpecificityLevel specificity_level = 3;
|
||||
}
|
||||
|
||||
enum SuccessRateSpecificityLevel {
|
||||
ENTITY = 0;
|
||||
GLOBAL = 1;
|
||||
}
|
||||
|
||||
message CalSuccessRateResponse {
|
||||
@ -31,12 +39,13 @@ message LabelWithScore {
|
||||
string label = 2;
|
||||
}
|
||||
|
||||
// API-2 types
|
||||
// API-2 types
|
||||
message UpdateSuccessRateWindowRequest {
|
||||
string id = 1;
|
||||
string params = 2;
|
||||
repeated LabelWithStatus labels_with_status = 3;
|
||||
UpdateSuccessRateWindowConfig config = 4;
|
||||
repeated LabelWithStatus global_labels_with_status = 5;
|
||||
}
|
||||
|
||||
message LabelWithStatus {
|
||||
@ -74,3 +83,22 @@ message InvalidateWindowsResponse {
|
||||
}
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user