[SMG]feat: implement TokenGuardBody for managing token return (#14653)
This commit is contained in:
@@ -41,6 +41,7 @@ serde_json = { version = "1.0", default-features = false, features = [
|
|||||||
] }
|
] }
|
||||||
serde_bytes = "0.11"
|
serde_bytes = "0.11"
|
||||||
bytes = "1.8.0"
|
bytes = "1.8.0"
|
||||||
|
http-body = "1.0"
|
||||||
rand = "0.9.2"
|
rand = "0.9.2"
|
||||||
reqwest = { version = "0.12.8", features = ["stream", "blocking", "json", "rustls-tls"], default-features = false }
|
reqwest = { version = "0.12.8", features = ["stream", "blocking", "json", "rustls-tls"], default-features = false }
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
use std::{
|
use std::{
|
||||||
|
pin::Pin,
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicU64, Ordering},
|
atomic::{AtomicU64, Ordering},
|
||||||
Arc,
|
Arc,
|
||||||
},
|
},
|
||||||
|
task::{Context, Poll},
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -13,6 +15,8 @@ use axum::{
|
|||||||
middleware::Next,
|
middleware::Next,
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
|
use bytes::Bytes;
|
||||||
|
use http_body::Frame;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use subtle::ConstantTimeEq;
|
use subtle::ConstantTimeEq;
|
||||||
use tokio::sync::{mpsc, oneshot};
|
use tokio::sync::{mpsc, oneshot};
|
||||||
@@ -36,6 +40,75 @@ use crate::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// A body wrapper that holds a token and returns it when the body is fully consumed or dropped.
|
||||||
|
/// This ensures that for streaming responses, the token is only returned after the entire
|
||||||
|
/// stream has been sent to the client.
|
||||||
|
pub struct TokenGuardBody {
|
||||||
|
inner: Body,
|
||||||
|
/// The token bucket to return tokens to. Uses Option so we can take() on drop.
|
||||||
|
token_bucket: Option<Arc<TokenBucket>>,
|
||||||
|
/// Number of tokens to return.
|
||||||
|
tokens: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TokenGuardBody {
|
||||||
|
/// Create a new TokenGuardBody that will return tokens when dropped.
|
||||||
|
pub fn new(inner: Body, token_bucket: Arc<TokenBucket>, tokens: f64) -> Self {
|
||||||
|
Self {
|
||||||
|
inner,
|
||||||
|
token_bucket: Some(token_bucket),
|
||||||
|
tokens,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TokenGuardBody {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Some(bucket) = self.token_bucket.take() {
|
||||||
|
let tokens = self.tokens;
|
||||||
|
debug!(
|
||||||
|
"TokenGuardBody: stream ended, returning {} tokens to bucket",
|
||||||
|
tokens
|
||||||
|
);
|
||||||
|
if let Ok(handle) = tokio::runtime::Handle::try_current() {
|
||||||
|
handle.spawn(async move {
|
||||||
|
bucket.return_tokens(tokens).await;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Runtime not available (e.g., during shutdown)
|
||||||
|
// Tokens will be lost, but this is acceptable during shutdown
|
||||||
|
warn!(
|
||||||
|
"TokenGuardBody: Cannot return {} tokens - no Tokio runtime available",
|
||||||
|
tokens
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl http_body::Body for TokenGuardBody {
|
||||||
|
type Data = Bytes;
|
||||||
|
type Error = axum::Error;
|
||||||
|
|
||||||
|
fn poll_frame(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
|
||||||
|
// SAFETY: We never move the inner body, and Body is Unpin
|
||||||
|
// (it's a type alias for UnsyncBoxBody which is Unpin)
|
||||||
|
let this = self.get_mut();
|
||||||
|
Pin::new(&mut this.inner).poll_frame(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_end_stream(&self) -> bool {
|
||||||
|
self.inner.is_end_stream()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size_hint(&self) -> http_body::SizeHint {
|
||||||
|
self.inner.size_hint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AuthConfig {
|
pub struct AuthConfig {
|
||||||
pub api_key: Option<String>,
|
pub api_key: Option<String>,
|
||||||
@@ -152,14 +225,10 @@ where
|
|||||||
{
|
{
|
||||||
type Response = S::Response;
|
type Response = S::Response;
|
||||||
type Error = S::Error;
|
type Error = S::Error;
|
||||||
type Future = std::pin::Pin<
|
type Future =
|
||||||
Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + Send>,
|
Pin<Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + Send>>;
|
||||||
>;
|
|
||||||
|
|
||||||
fn poll_ready(
|
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||||
&mut self,
|
|
||||||
cx: &mut std::task::Context<'_>,
|
|
||||||
) -> std::task::Poll<Result<(), Self::Error>> {
|
|
||||||
self.inner.poll_ready(cx)
|
self.inner.poll_ready(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,10 +565,12 @@ pub async fn concurrency_limit_middleware(
|
|||||||
debug!("Acquired token immediately");
|
debug!("Acquired token immediately");
|
||||||
let response = next.run(request).await;
|
let response = next.run(request).await;
|
||||||
|
|
||||||
// Return the token to the bucket
|
// Wrap the response body with TokenGuardBody to return token when stream ends
|
||||||
token_bucket.return_tokens(1.0).await;
|
// This ensures that for streaming responses, the token is only returned
|
||||||
|
// after the entire stream has been sent to the client.
|
||||||
response
|
let (parts, body) = response.into_parts();
|
||||||
|
let guarded_body = TokenGuardBody::new(body, token_bucket, 1.0);
|
||||||
|
Response::from_parts(parts, Body::new(guarded_body))
|
||||||
} else {
|
} else {
|
||||||
// No tokens available, try to queue if enabled
|
// No tokens available, try to queue if enabled
|
||||||
if let Some(queue_tx) = &app_state.concurrency_queue_tx {
|
if let Some(queue_tx) = &app_state.concurrency_queue_tx {
|
||||||
@@ -535,10 +606,10 @@ pub async fn concurrency_limit_middleware(
|
|||||||
|
|
||||||
let response = next.run(request).await;
|
let response = next.run(request).await;
|
||||||
|
|
||||||
// Return the token to the bucket
|
// Wrap the response body with TokenGuardBody to return token when stream ends
|
||||||
token_bucket.return_tokens(1.0).await;
|
let (parts, body) = response.into_parts();
|
||||||
|
let guarded_body = TokenGuardBody::new(body, token_bucket, 1.0);
|
||||||
response
|
Response::from_parts(parts, Body::new(guarded_body))
|
||||||
}
|
}
|
||||||
Ok(Err(status)) => {
|
Ok(Err(status)) => {
|
||||||
warn!("Queue returned error status: {}", status);
|
warn!("Queue returned error status: {}", status);
|
||||||
|
|||||||
Reference in New Issue
Block a user