use bytes::Bytes;
use http::{Request as HttpRequest, StatusCode};
use rate_limiter_aimd::{
Error as CrateError,
adaptive_concurrency::{
AdaptiveConcurrencySettings,
http::HttpError as GenericHttpError,
layer::AdaptiveConcurrencyLimitLayer,
reqwest_integration::ReqwestService,
retries::{
ExponentialBackoff, ExponentialBackoffPolicy, JitterMode,
RetryAction,
RetryLogic,
},
},
};
use reqwest::Response as ReqwestResponse;
use serde_json::json;
use std::{borrow::Cow, env, time::Duration};
use tokio::time::sleep;
use tower::{Service, ServiceBuilder, ServiceExt, retry::RetryLayer};
use tracing::{Level, debug, error, info, warn};
use tracing_appender;
use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt};
const OPENAI_API_KEY_ENV_VAR: &str = "OPENAI_API_KEY";
const OPENAI_BASE_URL_ENV_VAR: &str = "OPENAI_API_BASE_URL";
const OPENAI_MODEL_NAME_ENV_VAR: &str = "OPENAI_MODEL_NAME";
const DEFAULT_OPENAI_BASE_URL: &str = "https://api.openai.com";
const CHAT_COMPLETIONS_PATH: &str = "/v1/chat/completions";
const LOG_FILE_NAME: &str = "openai_adaptive_concurrency.log";
const NUM_PROMPTS_TO_SEND: usize = 100;
const REQUEST_INTERVAL_MS: u64 = 20;
const DEFAULT_MODEL_NAME: &str = "Qwen/Qwen3-235B-A22B-FP8";
const MAX_INDIVIDUAL_REQUEST_ATTEMPTS: usize = 10; const INITIAL_RETRY_BACKOFF_MS: u64 = 1000; const EXPONENTIAL_BACKOFF_BASE: u64 = 2; const MAX_SINGLE_RETRY_DELAY_SECS: u64 = 60; const MAX_TOTAL_RETRY_DURATION_MINS: u64 = 5;
#[derive(Clone, Debug, Default)]
struct OpenAIRetryLogic;
impl RetryLogic for OpenAIRetryLogic {
type Error = GenericHttpError; type Response = ReqwestResponse;
fn is_retriable_error(&self, error: &Self::Error) -> bool {
match error {
GenericHttpError::Transport { source } => {
warn!(error_source=?source, "OpenAIRetryLogic: Retrying due to transport error");
true
}
GenericHttpError::Timeout => {
warn!("OpenAIRetryLogic: Retrying due to timeout error.");
true
}
GenericHttpError::ServerError { status, body } => {
let s = StatusCode::from_u16(*status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
if s.is_server_error() || s == StatusCode::TOO_MANY_REQUESTS {
warn!(%status, error_body=body.chars().take(100).collect::<String>(), "OpenAIRetryLogic: Retrying due to server error");
true
} else {
error!(%status, error_body=body.chars().take(100).collect::<String>(), "OpenAIRetryLogic: Not retrying server/client error");
false
}
}
GenericHttpError::InvalidRequest { .. }
| GenericHttpError::BuildRequest { .. }
| GenericHttpError::ClientError { .. } => {
error!(full_error=?error, "OpenAIRetryLogic: Not retrying client-side/invalid request error");
false
}
}
}
fn should_retry_response(&self, response: &Self::Response) -> RetryAction {
let status = response.status();
if status.is_success() {
RetryAction::Successful
} else if status == StatusCode::TOO_MANY_REQUESTS
|| status == StatusCode::SERVICE_UNAVAILABLE
|| status.is_server_error()
{
warn!(%status, "OpenAIRetryLogic: Instructing to retry due to response status");
RetryAction::Retry(Cow::Owned(format!(
"Server responded with status {}",
status
)))
} else if status.is_client_error() {
error!(%status, "OpenAIRetryLogic: Instructing not to retry due to client error status");
RetryAction::DontRetry(Cow::Owned(format!(
"Server responded with client error status {}",
status
)))
} else {
warn!(%status, "OpenAIRetryLogic: Instructing not to retry due to unhandled status");
RetryAction::DontRetry(Cow::Owned(format!(
"Server responded with unhandled status {}",
status
)))
}
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
let file_appender = tracing_appender::rolling::daily(".", LOG_FILE_NAME);
let (non_blocking_appender, _guard) = tracing_appender::non_blocking(file_appender);
let default_filter = "info,rate_limiter_aimd::adaptive_concurrency::stats=debug,rate_limiter_aimd::adaptive_concurrency::retries=debug"; let env_filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_filter));
let console_layer = fmt::layer()
.with_writer(std::io::stdout)
.with_ansi(true)
.with_target(true)
.with_level(true);
let file_layer = fmt::layer()
.with_writer(non_blocking_appender)
.with_ansi(false)
.with_target(true)
.with_level(true);
tracing_subscriber::registry()
.with(env_filter)
.with(console_layer)
.with(file_layer)
.try_init()?;
info!(
"Tracing initialized. Default filter: '{}'. Logging to console and {}",
default_filter, LOG_FILE_NAME
);
dotenvy::dotenv().ok();
info!("Attempted to load .env file");
let api_key = env::var(OPENAI_API_KEY_ENV_VAR).map_err(|e| {
format!(
"Missing OpenAI API key env var: {}: {}",
OPENAI_API_KEY_ENV_VAR, e
)
})?;
let base_url =
env::var(OPENAI_BASE_URL_ENV_VAR).unwrap_or_else(|_| DEFAULT_OPENAI_BASE_URL.to_string());
let chat_completions_url = format!("{}{}", base_url, CHAT_COMPLETIONS_PATH);
let model_name =
env::var(OPENAI_MODEL_NAME_ENV_VAR).unwrap_or_else(|_| DEFAULT_MODEL_NAME.to_string());
info!(target: "config", api_endpoint = %chat_completions_url, model = %model_name, api_key_prefix = %api_key[..std::cmp::min(8, api_key.len())]);
let reqwest_client = reqwest::Client::builder()
.timeout(Duration::from_secs(60))
.build()?;
let reqwest_service = ReqwestService::new_with_client(reqwest_client);
let ac_settings = AdaptiveConcurrencySettings::builder()
.initial_concurrency(2) .max_concurrency_limit(8) .ewma_alpha(0.3)
.decrease_ratio(0.70) .rtt_deviation_scale(1.25) .build();
info!(target: "config", adaptive_concurrency_settings = ?ac_settings);
let openai_retry_logic = OpenAIRetryLogic::default();
let backoff_iterator_config = ExponentialBackoff::new(
INITIAL_RETRY_BACKOFF_MS, EXPONENTIAL_BACKOFF_BASE, Some(Duration::from_secs(MAX_SINGLE_RETRY_DELAY_SECS)), )
.factor(1);
let retry_policy = ExponentialBackoffPolicy::new(
MAX_INDIVIDUAL_REQUEST_ATTEMPTS,
backoff_iterator_config,
openai_retry_logic.clone(), JitterMode::Full,
Some(Duration::from_secs(MAX_TOTAL_RETRY_DURATION_MINS * 60)), );
info!(target: "config",
retry_policy_max_attempts = MAX_INDIVIDUAL_REQUEST_ATTEMPTS,
initial_backoff_ms = INITIAL_RETRY_BACKOFF_MS,
exponential_base = EXPONENTIAL_BACKOFF_BASE,
max_single_retry_delay_s = MAX_SINGLE_RETRY_DELAY_SECS,
max_total_duration_mins = MAX_TOTAL_RETRY_DURATION_MINS
);
let concurrency_layer = AdaptiveConcurrencyLimitLayer::new(
None,
ac_settings,
openai_retry_logic, );
let retrying_reqwest_service = ServiceBuilder::new()
.layer(RetryLayer::new(retry_policy))
.service(reqwest_service);
let mut service = ServiceBuilder::new()
.layer(concurrency_layer)
.service(retrying_reqwest_service);
info!(
"Service initialized. Starting to send {} prompts...",
NUM_PROMPTS_TO_SEND
);
let mut join_handles = Vec::new();
let prompts: Vec<String> = (0..NUM_PROMPTS_TO_SEND)
.map(|i| format!("This is test prompt number {}. Please provide a short, concise answer about a random topic. Keep it under 30 words.", i + 1))
.collect();
for (i, prompt_content) in prompts.into_iter().enumerate() {
let mut cloned_service = service.clone(); let key_clone = api_key.clone();
let url_clone = chat_completions_url.clone();
let model_clone = model_name.clone();
let task_id = i;
let handle = tokio::spawn(async move {
let request_payload = json!({
"model": model_clone, "messages": [{"role": "user", "content": prompt_content}],
"max_tokens": 50, "temperature": 0.7
});
let body_bytes = Bytes::from(
serde_json::to_vec(&request_payload)
.map_err(|e| CrateError::from(format!("[T{}] PayloadErr: {}", task_id, e)))?,
);
let http_request = HttpRequest::builder()
.method("POST")
.uri(url_clone.as_str())
.header("Authorization", format!("Bearer {}", key_clone))
.header("Content-Type", "application/json")
.body(Some(body_bytes))
.map_err(|e| CrateError::from(format!("[T{}] BuildReqErr: {}", task_id, e)))?;
debug!("[T{}] Waiting for service readiness...", task_id);
if let Err(e) = cloned_service.ready().await {
let err_msg = format!("[T{}] NotReady: {:?}", task_id, e);
error!("{}", err_msg);
return Err(CrateError::from(err_msg));
}
info!("[T{}] Calling service...", task_id);
match cloned_service.call(http_request).await {
Ok(response) => {
let status = response.status();
let response_body_text = response
.text()
.await
.unwrap_or_else(|e| format!("ErrReadBody: {}", e));
if status.is_success() {
info!(
"[T{}] SUCCESS: {} Body: {}",
task_id, status, response_body_text
);
} else {
warn!(
"[T{}] API_ERR_FINAL: {} Body: {}",
task_id, status, response_body_text
);
}
}
Err(e) => error!("[T{}] SVC_ERR_FINAL: {:?}", task_id, e),
}
Ok::<(), CrateError>(())
});
join_handles.push(handle);
if REQUEST_INTERVAL_MS > 0 {
sleep(Duration::from_millis(REQUEST_INTERVAL_MS)).await;
}
}
for (idx, handle) in join_handles.into_iter().enumerate() {
match handle.await {
Ok(Ok(_)) => info!("[Main] Task {} completed.", idx),
Ok(Err(e)) => error!("[Main] Task {} failed with app error: {:?}", idx, e),
Err(e) => error!("[Main] Task {} panicked/cancelled: {:?}", idx, e),
}
}
info!(
"All {} prompts processed. Check {} for logs.",
NUM_PROMPTS_TO_SEND, LOG_FILE_NAME
);
Ok(())
}