mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-10 03:09:50 +08:00
feat(routing): add strategy failover controls
This commit is contained in:
@@ -300,6 +300,34 @@ pub(crate) fn classify_local_failover(
|
||||
policy: &LocalFailoverPolicy,
|
||||
input: LocalFailoverInput<'_>,
|
||||
) -> LocalFailoverClassification {
|
||||
if input.status_code >= 400
|
||||
&& policy.routing_rules.error_stop_patterns.iter().any(|rule| {
|
||||
failover_pattern_matches(
|
||||
&rule.pattern,
|
||||
&rule.status_codes,
|
||||
input.response_text,
|
||||
input.status_code,
|
||||
)
|
||||
})
|
||||
{
|
||||
return LocalFailoverClassification::StopErrorPattern;
|
||||
}
|
||||
if input.status_code == 200
|
||||
&& policy
|
||||
.routing_rules
|
||||
.success_failover_patterns
|
||||
.iter()
|
||||
.any(|rule| {
|
||||
failover_pattern_matches(
|
||||
&rule.pattern,
|
||||
&rule.status_codes,
|
||||
input.response_text,
|
||||
input.status_code,
|
||||
)
|
||||
})
|
||||
{
|
||||
return LocalFailoverClassification::RetrySuccessPattern;
|
||||
}
|
||||
if policy.stop_status_codes.contains(&input.status_code) {
|
||||
return LocalFailoverClassification::StopStatusCode;
|
||||
}
|
||||
@@ -487,13 +515,27 @@ fn local_failover_regex_rule_matches(
|
||||
response_text: Option<&str>,
|
||||
status_code: u16,
|
||||
) -> bool {
|
||||
if !rule.status_codes.is_empty() && !rule.status_codes.contains(&status_code) {
|
||||
failover_pattern_matches(
|
||||
&rule.pattern,
|
||||
&rule.status_codes,
|
||||
response_text,
|
||||
status_code,
|
||||
)
|
||||
}
|
||||
|
||||
fn failover_pattern_matches(
|
||||
pattern: &str,
|
||||
status_codes: &std::collections::BTreeSet<u16>,
|
||||
response_text: Option<&str>,
|
||||
status_code: u16,
|
||||
) -> bool {
|
||||
if !status_codes.is_empty() && !status_codes.contains(&status_code) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let pattern = rule.pattern.trim();
|
||||
let pattern = pattern.trim();
|
||||
if pattern.is_empty() {
|
||||
return !rule.status_codes.is_empty();
|
||||
return !status_codes.is_empty();
|
||||
}
|
||||
|
||||
let Some(response_text) = response_text else {
|
||||
@@ -507,6 +549,72 @@ fn local_failover_regex_rule_matches(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn routing_rules_precede_provider_rules_and_keep_provider_fallback() {
|
||||
let policy = super::LocalFailoverPolicy {
|
||||
routing_rules: aether_routing_core::RoutingFailoverRules {
|
||||
success_failover_patterns: vec![aether_routing_core::RoutingFailoverRule {
|
||||
pattern: "(?i)capacity.*exhausted".to_string(),
|
||||
..Default::default()
|
||||
}],
|
||||
error_stop_patterns: vec![aether_routing_core::RoutingFailoverRule {
|
||||
pattern: "invalid.*parameter".to_string(),
|
||||
status_codes: [400].into_iter().collect(),
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
stop_status_codes: [200, 403].into_iter().collect(),
|
||||
continue_status_codes: [400].into_iter().collect(),
|
||||
..Default::default()
|
||||
};
|
||||
for (status, body, expected) in [
|
||||
(
|
||||
200,
|
||||
"CAPACITY exhausted",
|
||||
super::LocalFailoverClassification::RetrySuccessPattern,
|
||||
),
|
||||
(
|
||||
400,
|
||||
"invalid request parameter",
|
||||
super::LocalFailoverClassification::StopErrorPattern,
|
||||
),
|
||||
(
|
||||
400,
|
||||
"capacity exhausted",
|
||||
super::LocalFailoverClassification::RetryStatusCode,
|
||||
),
|
||||
(
|
||||
403,
|
||||
"permission denied",
|
||||
super::LocalFailoverClassification::StopStatusCode,
|
||||
),
|
||||
(
|
||||
429,
|
||||
"rate limited",
|
||||
super::LocalFailoverClassification::RetryUpstreamFailure,
|
||||
),
|
||||
] {
|
||||
assert_eq!(
|
||||
super::classify_local_failover(
|
||||
&policy,
|
||||
super::LocalFailoverInput::new(status, Some(body))
|
||||
),
|
||||
expected
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn routing_transport_stop_cannot_be_overridden_by_provider() {
|
||||
let policy = super::LocalFailoverPolicy {
|
||||
stop_on_transport_errors: true,
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
super::classify_local_transport_error(&policy),
|
||||
super::LocalTransportFailoverClassification::StopTransportError
|
||||
);
|
||||
}
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use super::{
|
||||
|
||||
@@ -4,7 +4,7 @@ use aether_contracts::ExecutionPlan;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::debug;
|
||||
|
||||
use aether_routing_core::RoutingExecutionPolicy;
|
||||
use aether_routing_core::{RoutingExecutionPolicy, RoutingFailoverRules};
|
||||
|
||||
use crate::provider_transport::GatewayProviderTransportSnapshot;
|
||||
use crate::AppState;
|
||||
@@ -14,6 +14,7 @@ pub(crate) const ROUTING_EXECUTION_POLICY_REPORT_FIELD: &str = "routing_executio
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct LocalFailoverPolicy {
|
||||
pub(crate) routing_rules: RoutingFailoverRules,
|
||||
pub(crate) max_retries: Option<u64>,
|
||||
pub(crate) max_transfer_count: u64,
|
||||
pub(crate) max_transfer_timeout_seconds: u64,
|
||||
@@ -29,6 +30,7 @@ pub(crate) struct LocalFailoverPolicy {
|
||||
impl Default for LocalFailoverPolicy {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
routing_rules: RoutingFailoverRules::default(),
|
||||
max_retries: None,
|
||||
max_transfer_count: 0,
|
||||
max_transfer_timeout_seconds: 0,
|
||||
@@ -61,8 +63,10 @@ pub(crate) async fn resolve_local_failover_policy(
|
||||
Ok(Some(transport)) => local_failover_policy_from_transport(&transport),
|
||||
Ok(None) | Err(_) => LocalFailoverPolicy::default(),
|
||||
};
|
||||
let cyber_continue_failover = routing_execution_policy_from_report_context(report_context)
|
||||
.is_some_and(|policy| policy.cyber_continue_failover);
|
||||
let routing_policy =
|
||||
routing_execution_policy_from_report_context(report_context).unwrap_or_default();
|
||||
let cyber_continue_failover = routing_policy.cyber_continue_failover;
|
||||
policy.routing_rules = routing_policy.failover_rules;
|
||||
policy.stop_cyber_policy_errors = !cyber_continue_failover;
|
||||
debug!(
|
||||
event_name = "local_failover_policy_loaded",
|
||||
@@ -80,6 +84,8 @@ pub(crate) async fn resolve_local_failover_policy(
|
||||
stop_on_transport_errors = policy.stop_on_transport_errors,
|
||||
success_failover_pattern_count = policy.success_failover_patterns.len(),
|
||||
error_stop_pattern_count = policy.error_stop_patterns.len(),
|
||||
global_success_pattern_count = policy.routing_rules.success_failover_patterns.len(),
|
||||
global_stop_pattern_count = policy.routing_rules.error_stop_patterns.len(),
|
||||
cyber_continue_failover,
|
||||
"gateway loaded local failover policy from transport snapshot"
|
||||
);
|
||||
@@ -122,6 +128,7 @@ pub(crate) fn local_failover_policy_from_transport(
|
||||
});
|
||||
|
||||
LocalFailoverPolicy {
|
||||
routing_rules: RoutingFailoverRules::default(),
|
||||
max_retries,
|
||||
max_transfer_count: provider_config
|
||||
.and_then(|value| value.get("max_transfer_count"))
|
||||
@@ -184,6 +191,10 @@ pub(crate) fn local_failover_policy_from_report_context(
|
||||
.as_object()?;
|
||||
|
||||
Some(LocalFailoverPolicy {
|
||||
routing_rules: object
|
||||
.get("routing_rules")
|
||||
.and_then(|value| serde_json::from_value(value.clone()).ok())
|
||||
.unwrap_or_default(),
|
||||
max_retries: object.get("max_retries").and_then(parse_u64_value),
|
||||
max_transfer_count: object
|
||||
.get("max_transfer_count")
|
||||
@@ -267,6 +278,7 @@ fn parse_status_code_list(value: &Value) -> BTreeSet<u16> {
|
||||
|
||||
fn local_failover_policy_to_value(policy: &LocalFailoverPolicy) -> Value {
|
||||
json!({
|
||||
"routing_rules": policy.routing_rules,
|
||||
"max_retries": policy.max_retries,
|
||||
"max_transfer_count": policy.max_transfer_count,
|
||||
"max_transfer_timeout_seconds": policy.max_transfer_timeout_seconds,
|
||||
@@ -525,6 +537,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
local_failover_policy_from_report_context(Some(&report_context)),
|
||||
Some(LocalFailoverPolicy {
|
||||
routing_rules: Default::default(),
|
||||
max_retries: Some(2),
|
||||
max_transfer_count: 10,
|
||||
max_transfer_timeout_seconds: 60,
|
||||
|
||||
Reference in New Issue
Block a user