2026-07-27 22:06:28 +08:00
|
|
|
use aether_routing_core::ResolvedRoutingPolicy;
|
2026-04-27 13:39:50 +08:00
|
|
|
use aether_scheduler_core::{
|
2026-07-27 22:06:28 +08:00
|
|
|
build_scheduler_affinity_cache_key_for_api_key_id_with_client_session_and_scope,
|
|
|
|
|
ClientSessionAffinity, SchedulerAffinityScope, SchedulerAffinityTarget,
|
|
|
|
|
SchedulerMinimalCandidateSelectionCandidate,
|
2026-04-27 13:39:50 +08:00
|
|
|
};
|
|
|
|
|
|
2026-05-02 13:23:54 +08:00
|
|
|
use crate::ai_serving::{GatewayAuthApiKeySnapshot, PlannerAppState};
|
2026-04-27 13:39:50 +08:00
|
|
|
use crate::scheduler::affinity::SCHEDULER_AFFINITY_TTL;
|
|
|
|
|
|
|
|
|
|
const PLANNER_SCHEDULER_AFFINITY_MAX_ENTRIES: usize = 10_000;
|
|
|
|
|
|
2026-06-24 01:47:26 +08:00
|
|
|
pub(crate) fn has_explicit_session_affinity(
|
|
|
|
|
client_session_affinity: Option<&ClientSessionAffinity>,
|
|
|
|
|
) -> bool {
|
|
|
|
|
client_session_affinity.is_some_and(ClientSessionAffinity::has_session_key)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 13:39:50 +08:00
|
|
|
pub(crate) fn read_cached_scheduler_affinity_target(
|
|
|
|
|
state: PlannerAppState<'_>,
|
|
|
|
|
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
|
2026-05-05 11:23:54 +08:00
|
|
|
client_session_affinity: Option<&ClientSessionAffinity>,
|
2026-04-27 13:39:50 +08:00
|
|
|
client_api_format: &str,
|
|
|
|
|
requested_model: Option<&str>,
|
2026-07-27 22:06:28 +08:00
|
|
|
routing_policy: Option<&ResolvedRoutingPolicy>,
|
2026-04-27 13:39:50 +08:00
|
|
|
) -> Option<SchedulerAffinityTarget> {
|
2026-06-24 01:47:26 +08:00
|
|
|
if !has_explicit_session_affinity(client_session_affinity) {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
2026-04-27 13:39:50 +08:00
|
|
|
let requested_model = requested_model
|
|
|
|
|
.map(str::trim)
|
|
|
|
|
.filter(|value| !value.is_empty())?;
|
|
|
|
|
let api_key_id = auth_snapshot
|
|
|
|
|
.map(|snapshot| snapshot.api_key_id.trim())
|
|
|
|
|
.filter(|value| !value.is_empty())?;
|
2026-07-27 22:06:28 +08:00
|
|
|
let affinity_scope = scheduler_affinity_scope_for_routing_policy(routing_policy);
|
|
|
|
|
let cache_key =
|
|
|
|
|
build_scheduler_affinity_cache_key_for_api_key_id_with_client_session_and_scope(
|
|
|
|
|
api_key_id,
|
|
|
|
|
client_api_format,
|
|
|
|
|
requested_model,
|
|
|
|
|
client_session_affinity,
|
|
|
|
|
affinity_scope.as_ref(),
|
|
|
|
|
)?;
|
2026-04-27 13:39:50 +08:00
|
|
|
|
|
|
|
|
state
|
|
|
|
|
.app()
|
|
|
|
|
.read_scheduler_affinity_target(&cache_key, SCHEDULER_AFFINITY_TTL)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) fn remember_scheduler_affinity_for_candidate(
|
|
|
|
|
state: PlannerAppState<'_>,
|
|
|
|
|
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
|
2026-05-05 11:23:54 +08:00
|
|
|
client_session_affinity: Option<&ClientSessionAffinity>,
|
2026-04-27 13:39:50 +08:00
|
|
|
client_api_format: &str,
|
|
|
|
|
requested_model: &str,
|
|
|
|
|
candidate: &SchedulerMinimalCandidateSelectionCandidate,
|
2026-05-11 01:45:49 +08:00
|
|
|
) {
|
2026-06-24 01:47:26 +08:00
|
|
|
if !has_explicit_session_affinity(client_session_affinity) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-05-11 01:45:49 +08:00
|
|
|
remember_scheduler_affinity_for_candidate_at_epoch(
|
|
|
|
|
state,
|
|
|
|
|
auth_snapshot,
|
|
|
|
|
client_session_affinity,
|
|
|
|
|
client_api_format,
|
|
|
|
|
requested_model,
|
|
|
|
|
candidate,
|
|
|
|
|
None,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) fn remember_scheduler_affinity_for_candidate_at_epoch(
|
|
|
|
|
state: PlannerAppState<'_>,
|
|
|
|
|
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
|
|
|
|
|
client_session_affinity: Option<&ClientSessionAffinity>,
|
|
|
|
|
client_api_format: &str,
|
|
|
|
|
requested_model: &str,
|
|
|
|
|
candidate: &SchedulerMinimalCandidateSelectionCandidate,
|
|
|
|
|
expected_epoch: Option<u64>,
|
2026-07-27 22:06:28 +08:00
|
|
|
) {
|
|
|
|
|
remember_scheduler_affinity_for_candidate_with_scope_at_epoch(
|
|
|
|
|
state,
|
|
|
|
|
auth_snapshot,
|
|
|
|
|
client_session_affinity,
|
|
|
|
|
client_api_format,
|
|
|
|
|
requested_model,
|
|
|
|
|
candidate,
|
|
|
|
|
None,
|
|
|
|
|
expected_epoch,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
|
|
|
pub(crate) fn remember_scheduler_affinity_for_candidate_with_routing_policy_at_epoch(
|
|
|
|
|
state: PlannerAppState<'_>,
|
|
|
|
|
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
|
|
|
|
|
client_session_affinity: Option<&ClientSessionAffinity>,
|
|
|
|
|
client_api_format: &str,
|
|
|
|
|
requested_model: &str,
|
|
|
|
|
candidate: &SchedulerMinimalCandidateSelectionCandidate,
|
|
|
|
|
routing_policy: Option<&ResolvedRoutingPolicy>,
|
|
|
|
|
expected_epoch: Option<u64>,
|
|
|
|
|
) {
|
|
|
|
|
let affinity_scope = scheduler_affinity_scope_for_routing_policy(routing_policy);
|
|
|
|
|
remember_scheduler_affinity_for_candidate_with_scope_at_epoch(
|
|
|
|
|
state,
|
|
|
|
|
auth_snapshot,
|
|
|
|
|
client_session_affinity,
|
|
|
|
|
client_api_format,
|
|
|
|
|
requested_model,
|
|
|
|
|
candidate,
|
|
|
|
|
affinity_scope.as_ref(),
|
|
|
|
|
expected_epoch,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
|
|
|
fn remember_scheduler_affinity_for_candidate_with_scope_at_epoch(
|
|
|
|
|
state: PlannerAppState<'_>,
|
|
|
|
|
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
|
|
|
|
|
client_session_affinity: Option<&ClientSessionAffinity>,
|
|
|
|
|
client_api_format: &str,
|
|
|
|
|
requested_model: &str,
|
|
|
|
|
candidate: &SchedulerMinimalCandidateSelectionCandidate,
|
|
|
|
|
affinity_scope: Option<&SchedulerAffinityScope>,
|
|
|
|
|
expected_epoch: Option<u64>,
|
2026-04-27 13:39:50 +08:00
|
|
|
) {
|
2026-06-24 01:47:26 +08:00
|
|
|
if !has_explicit_session_affinity(client_session_affinity) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-04-27 13:39:50 +08:00
|
|
|
let Some(api_key_id) = auth_snapshot
|
|
|
|
|
.map(|snapshot| snapshot.api_key_id.trim())
|
|
|
|
|
.filter(|value| !value.is_empty())
|
|
|
|
|
else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
2026-07-27 22:06:28 +08:00
|
|
|
let Some(cache_key) =
|
|
|
|
|
build_scheduler_affinity_cache_key_for_api_key_id_with_client_session_and_scope(
|
|
|
|
|
api_key_id,
|
|
|
|
|
client_api_format,
|
|
|
|
|
requested_model,
|
|
|
|
|
client_session_affinity,
|
|
|
|
|
affinity_scope,
|
|
|
|
|
)
|
|
|
|
|
else {
|
2026-04-27 13:39:50 +08:00
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-11 01:45:49 +08:00
|
|
|
let _ = state.app().remember_scheduler_affinity_target_for_epoch(
|
2026-04-27 13:39:50 +08:00
|
|
|
&cache_key,
|
|
|
|
|
SchedulerAffinityTarget {
|
|
|
|
|
provider_id: candidate.provider_id.clone(),
|
|
|
|
|
endpoint_id: candidate.endpoint_id.clone(),
|
|
|
|
|
key_id: candidate.key_id.clone(),
|
|
|
|
|
},
|
|
|
|
|
SCHEDULER_AFFINITY_TTL,
|
|
|
|
|
PLANNER_SCHEDULER_AFFINITY_MAX_ENTRIES,
|
2026-05-11 01:45:49 +08:00
|
|
|
expected_epoch,
|
2026-04-27 13:39:50 +08:00
|
|
|
);
|
|
|
|
|
}
|
2026-07-27 22:06:28 +08:00
|
|
|
|
|
|
|
|
fn scheduler_affinity_scope_for_routing_policy(
|
|
|
|
|
routing_policy: Option<&ResolvedRoutingPolicy>,
|
|
|
|
|
) -> Option<SchedulerAffinityScope> {
|
|
|
|
|
let policy = routing_policy?;
|
|
|
|
|
let group_id = policy
|
|
|
|
|
.group_id
|
|
|
|
|
.as_deref()
|
|
|
|
|
.map(str::trim)
|
|
|
|
|
.filter(|group_id| !group_id.is_empty())?;
|
|
|
|
|
Some(SchedulerAffinityScope::new(group_id, policy.group_version))
|
|
|
|
|
}
|