Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(routing): Integrate global success rates #6950

Merged
merged 19 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions api-reference/openapi_spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -25162,9 +25162,19 @@
}
],
"nullable": true
},
"specificity_level": {
"$ref": "#/components/schemas/SuccessRateSpecificityLevel"
}
}
},
"SuccessRateSpecificityLevel": {
"type": "string",
"enum": [
"merchant",
"global"
]
},
"SupportedPaymentMethod": {
"type": "object",
"required": [
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 {
Sarthak1799 marked this conversation as resolved.
Show resolved Hide resolved
#[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 @@ -419,6 +419,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 @@ -431,6 +431,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,21 +1,24 @@
use api_models::routing::{
CurrentBlockThreshold, RoutableConnectorChoice, RoutableConnectorChoiceWithStatus,
SuccessBasedRoutingConfig, SuccessBasedRoutingConfigBody,
SuccessBasedRoutingConfig, SuccessBasedRoutingConfigBody, SuccessRateSpecificityLevel,
};
use common_utils::{ext_traits::OptionExt, transformers::ForeignTryFrom};
use error_stack::ResultExt;
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(
missing_docs,
unused_qualifications,
clippy::unwrap_used,
clippy::as_conversions
clippy::as_conversions,
clippy::use_self
)]
pub mod success_rate {
tonic::include_proto!("success_rate");
Expand Down Expand Up @@ -49,6 +52,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 @@ -106,18 +118,28 @@ 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 mut request = tonic::Request::new(UpdateSuccessRateWindowRequest {
id,
params,
params: params.clone(),
labels_with_status,
config,
global_labels_with_status,
});

request.add_headers_to_grpc_request(headers);
Expand Down Expand Up @@ -152,6 +174,52 @@ impl SuccessBasedDynamicRouting for SuccessRateCalculatorClient<Client> {
.into_inner();
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 mut request = tonic::Request::new(CalGlobalSuccessRateRequest {
entity_id: id,
entity_params: params,
entity_labels: labels,
global_labels,
config,
});

request.add_headers_to_grpc_request(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();

Ok(response)
}
}

impl ForeignTryFrom<CurrentBlockThreshold> for DynamicCurrentThreshold {
Expand Down Expand Up @@ -203,6 +271,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 @@ -709,7 +709,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 @@ -725,28 +725,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 {
Sarthak1799 marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -755,7 +761,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 @@ -765,6 +772,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 @@ -783,8 +793,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",
Sarthak1799 marked this conversation as resolved.
Show resolved Hide resolved
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
Loading