mod health;
#[cfg(test)]
mod tests;
use std::fmt;
use std::sync::{Arc, Mutex};
use std::time::Instant;
use async_trait::async_trait;
use futures_util::future::BoxFuture;
use crate::definition::ProviderDefinition;
use crate::error::ProviderError;
use crate::model::ModelInfo;
use crate::registry::{Provider, ProviderSession, ProviderSessionScope};
use crate::request::{CompactionRequest, MemorySummarizeRequest, Request};
use crate::response::{CompactionResponse, MemorySummarizeResponse};
use crate::stream::ProviderEventStream;
pub use self::health::{FailureKind, GatewayRingPolicy};
use self::health::{RingHealth, Selection};
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("a gateway ring needs at least one member")]
pub struct EmptyGatewayRing;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GatewayMemberRef {
pub index: usize,
pub label: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GatewayRingEvent {
MemberFailed {
member: GatewayMemberRef,
kind: FailureKind,
consecutive_failures: u32,
error: String,
},
Rotated {
from: GatewayMemberRef,
to: GatewayMemberRef,
after_failures: u32,
},
Returned { member: GatewayMemberRef },
Recovered { member: GatewayMemberRef },
}
type Observer = Arc<dyn Fn(GatewayRingEvent) + Send + Sync>;
struct Member {
provider: Arc<dyn Provider>,
label: String,
}
impl Member {
fn new(provider: Arc<dyn Provider>) -> Self {
let definition = provider.definition();
let label = definition
.descriptor
.display_name
.clone()
.or_else(|| definition.base_url.clone())
.unwrap_or_else(|| definition.descriptor.id.to_string());
Self { provider, label }
}
fn reference(&self, index: usize) -> GatewayMemberRef {
GatewayMemberRef {
index,
label: self.label.clone(),
}
}
}
#[derive(Clone)]
pub struct GatewayRing {
members: Arc<[Member]>,
definition: ProviderDefinition,
policy: GatewayRingPolicy,
health: Arc<Mutex<RingHealth>>,
observer: Option<Observer>,
}
impl fmt::Debug for GatewayRing {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("GatewayRing")
.field(
"members",
&self
.members
.iter()
.map(|member| member.label.as_str())
.collect::<Vec<_>>(),
)
.field("policy", &self.policy)
.field("current", &self.health().current())
.finish_non_exhaustive()
}
}
impl GatewayRing {
pub fn new(
members: impl IntoIterator<Item = Arc<dyn Provider>>,
policy: GatewayRingPolicy,
) -> Result<Self, EmptyGatewayRing> {
let members: Arc<[Member]> = members.into_iter().map(Member::new).collect();
let first = members.first().ok_or(EmptyGatewayRing)?;
let definition = ring_definition(first.provider.definition(), &members);
Ok(Self {
health: Arc::new(Mutex::new(RingHealth::new(members.len()))),
members,
definition,
policy,
observer: None,
})
}
#[must_use]
pub fn with_observer(
self,
observer: impl Fn(GatewayRingEvent) + Send + Sync + 'static,
) -> Self {
Self {
observer: Some(Arc::new(observer)),
..self
}
}
pub fn policy(&self) -> GatewayRingPolicy {
self.policy
}
pub fn members(&self) -> Vec<GatewayMemberRef> {
self.members
.iter()
.enumerate()
.map(|(index, member)| member.reference(index))
.collect()
}
pub fn active(&self) -> GatewayMemberRef {
let index = self.health().current();
self.members[index].reference(index)
}
fn health(&self) -> RingHealth {
self.health
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
fn transition<T>(&self, transition: impl FnOnce(&RingHealth) -> (RingHealth, T)) -> T {
let mut guard = self
.health
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let (next, out) = transition(&guard);
*guard = next;
out
}
fn emit(&self, event: GatewayRingEvent) {
if let Some(observer) = &self.observer {
observer(event);
}
}
async fn with_rotation<A, T>(
&self,
arg: A,
op: impl for<'a> Fn(&'a dyn Provider, &'a A) -> BoxFuture<'a, Result<T, ProviderError>>,
) -> Result<T, ProviderError>
where
A: Sync,
{
let mut selection = self.transition(|health| health.select(self.policy, Instant::now()));
let mut attempts = 0usize;
loop {
attempts += 1;
let index = selection.index;
let member = &self.members[index];
if selection.returned {
self.emit(GatewayRingEvent::Returned {
member: member.reference(index),
});
}
let probation = self.health().on_probation();
match op(member.provider.as_ref(), &arg).await {
Ok(value) => {
self.transition(|health| (health.succeeded(index), ()));
if probation {
self.emit(GatewayRingEvent::Recovered {
member: member.reference(index),
});
}
return Ok(value);
}
Err(error) => {
let kind = classify(&error);
if kind == FailureKind::NotTheMembersFault {
return Err(error);
}
let now = Instant::now();
let rotation =
self.transition(|health| health.failed(index, kind, self.policy, now));
self.emit(GatewayRingEvent::MemberFailed {
member: member.reference(index),
kind,
consecutive_failures: rotation
.map_or_else(|| self.streak_of(index), |r| r.after_failures),
error: error.to_string(),
});
let Some(rotation) = rotation else {
return Err(error);
};
self.emit(GatewayRingEvent::Rotated {
from: member.reference(rotation.from),
to: self.members[rotation.to].reference(rotation.to),
after_failures: rotation.after_failures,
});
if attempts >= self.members.len() {
return Err(error);
}
selection = Selection {
index: rotation.to,
returned: false,
};
}
}
}
}
fn streak_of(&self, index: usize) -> u32 {
self.health().streak_of(index)
}
}
pub fn classify(error: &ProviderError) -> FailureKind {
use reqwest::StatusCode;
match error {
ProviderError::Transport(_)
| ProviderError::Decode(_)
| ProviderError::Deserialize(_)
| ProviderError::Retryable { .. }
| ProviderError::InvalidResponse(_)
| ProviderError::MalformedStream(_) => FailureKind::Counted,
ProviderError::Http { status, .. } => {
if *status == StatusCode::TOO_MANY_REQUESTS || *status == StatusCode::REQUEST_TIMEOUT {
FailureKind::Counted
} else if status.is_client_error() {
FailureKind::Immediate
} else {
FailureKind::Counted
}
}
ProviderError::ContextLengthExceeded { .. }
| ProviderError::Serialize(_)
| ProviderError::InvalidRequest(_)
| ProviderError::UnsupportedCapability(_) => FailureKind::NotTheMembersFault,
}
}
fn ring_definition(first: ProviderDefinition, members: &[Member]) -> ProviderDefinition {
let labels = members
.iter()
.map(|member| member.label.as_str())
.collect::<Vec<_>>()
.join(" → ");
let mut definition = first;
definition.descriptor.display_name = Some(format!("gateway ring: {labels}"));
definition
}
#[async_trait]
impl Provider for GatewayRing {
async fn list_models(&self) -> Result<Vec<ModelInfo>, ProviderError> {
self.with_rotation((), |member, ()| member.list_models())
.await
}
async fn create_session(&self) -> Result<Box<dyn ProviderSession>, ProviderError> {
Ok(Box::new(GatewayRingSession(self.clone())))
}
fn definition(&self) -> ProviderDefinition {
self.definition.clone()
}
fn fresh_session_scope(&self) -> Result<ProviderSessionScope, ProviderError> {
let members = self
.members
.iter()
.map(|member| {
member.provider.fresh_session_scope().map(|scope| Member {
provider: Arc::new(scope),
label: member.label.clone(),
})
})
.collect::<Result<Arc<[Member]>, _>>()?;
Ok(ProviderSessionScope::new(Self {
members,
definition: self.definition.clone(),
policy: self.policy,
health: Arc::clone(&self.health),
observer: self.observer.clone(),
}))
}
async fn stream(&self, request: Request<'_>) -> Result<ProviderEventStream, ProviderError> {
self.with_rotation(request, |member, request| member.stream(request.clone()))
.await
}
async fn compact(
&self,
request: CompactionRequest<'_>,
) -> Result<CompactionResponse, ProviderError> {
self.with_rotation(request, |member, request| member.compact(request.clone()))
.await
}
async fn summarize_memories(
&self,
request: MemorySummarizeRequest<'_>,
) -> Result<MemorySummarizeResponse, ProviderError> {
self.with_rotation(request, |member, request| {
member.summarize_memories(request.clone())
})
.await
}
}
struct GatewayRingSession(GatewayRing);
#[async_trait]
impl ProviderSession for GatewayRingSession {
async fn stream(&self, request: Request<'_>) -> Result<ProviderEventStream, ProviderError> {
Provider::stream(&self.0, request).await
}
async fn compact(
&self,
request: CompactionRequest<'_>,
) -> Result<CompactionResponse, ProviderError> {
Provider::compact(&self.0, request).await
}
async fn summarize_memories(
&self,
request: MemorySummarizeRequest<'_>,
) -> Result<MemorySummarizeResponse, ProviderError> {
Provider::summarize_memories(&self.0, request).await
}
}