Skip to content

Commit

Permalink
feat(routing): Integrate global success rates (#6950)
Browse files Browse the repository at this point in the history
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
  • Loading branch information
Sarthak1799 and hyperswitch-bot[bot] authored Jan 21, 2025
1 parent 100a178 commit 39d2d6c
Show file tree
Hide file tree
Showing 11 changed files with 196 additions and 17 deletions.
10 changes: 10 additions & 0 deletions api-reference/openapi_spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -25202,9 +25202,19 @@
}
],
"nullable": true
},
"specificity_level": {
"$ref": "#/components/schemas/SuccessRateSpecificityLevel"
}
}
},
"SuccessRateSpecificityLevel": {
"type": "string",
"enum": [
"merchant",
"global"
]
},
"SupportedPaymentMethod": {
"allOf": [
{
Expand Down
12 changes: 12 additions & 0 deletions crates/api_models/src/routing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,7 @@ impl Default for SuccessBasedRoutingConfig {
duration_in_mins: Some(5),
max_total_count: Some(2),
}),
specificity_level: SuccessRateSpecificityLevel::default(),
}),
}
}
Expand All @@ -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)]
Expand All @@ -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,
Expand Down Expand Up @@ -849,6 +860,7 @@ impl SuccessBasedRoutingConfigBody {
.as_mut()
.map(|threshold| threshold.update(current_block_threshold));
}
self.specificity_level = new.specificity_level
}
}

Expand Down
2 changes: 2 additions & 0 deletions crates/diesel_models/src/dynamic_routing_stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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>,
}
2 changes: 2 additions & 0 deletions crates/diesel_models/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
}
}

Expand Down
2 changes: 2 additions & 0 deletions crates/diesel_models/src/schema_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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(
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -113,19 +124,29 @@ 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(),
status: conn_choice.status,
})
.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,
);
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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(),
})?,
})
}
}
1 change: 1 addition & 0 deletions crates/openapi/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
46 changes: 34 additions & 12 deletions crates/router/src/core/routing/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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 {
Expand All @@ -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,
Expand All @@ -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(
Expand All @@ -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()),
(
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- Your SQL goes here
ALTER TABLE dynamic_routing_stats
ADD COLUMN IF NOT EXISTS global_success_based_connector VARCHAR(64);
Loading

0 comments on commit 39d2d6c

Please sign in to comment.