1use crate::error::ProxyError as ProxyLifecycleError;
6use async_stream::try_stream;
7use axum::Json;
8use axum::body::Body;
9use axum::http::{HeaderMap, Response, StatusCode, header};
10use axum::response::IntoResponse;
11use bytes::Bytes;
12use futures_util::{FutureExt, Stream, StreamExt};
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15use std::env;
16use std::fmt;
17use std::future::Future;
18use std::sync::atomic::{AtomicUsize, Ordering};
19use std::time::Duration;
20use tokio::task::JoinHandle;
21
22pub fn run<F, Fut>(run_async: F) -> Result<(), ProxyLifecycleError>
24where
25 F: FnOnce() -> Fut,
26 Fut: Future<Output = Result<(), ProxyLifecycleError>>,
27{
28 let runtime = tokio::runtime::Builder::new_multi_thread()
29 .enable_all()
30 .build()
31 .map_err(|error| ProxyLifecycleError::Lifecycle {
32 message: format!("failed to create proxy tokio runtime: {error}"),
33 })?;
34 runtime.block_on(run_async())
35}
36
37#[derive(Serialize)]
39pub struct ProxyHealthcheckResponse {
40 pub ready: bool,
41 pub prefill_instances: usize,
42 pub decode_instances: usize,
43}
44
45pub(crate) fn healthcheck_response(
47 ready: bool,
48 prefill_instances: usize,
49 decode_instances: usize,
50) -> (StatusCode, Json<ProxyHealthcheckResponse>) {
51 let status = if ready {
52 StatusCode::OK
53 } else {
54 StatusCode::SERVICE_UNAVAILABLE
55 };
56 (
57 status,
58 Json(ProxyHealthcheckResponse {
59 ready,
60 prefill_instances,
61 decode_instances,
62 }),
63 )
64}
65
66pub(crate) fn require_endpoints(
68 proxy_name: &'static str,
69 prefill_is_empty: bool,
70 decode_is_empty: bool,
71) -> Result<(), ProxyLifecycleError> {
72 if prefill_is_empty {
73 return Err(ProxyLifecycleError::Invalid {
74 message: format!("{proxy_name} requires at least one prefill endpoint"),
75 });
76 }
77 if decode_is_empty {
78 return Err(ProxyLifecycleError::Invalid {
79 message: format!("{proxy_name} requires at least one decode endpoint"),
80 });
81 }
82 Ok(())
83}
84
85pub(crate) fn pooled_client(
87 proxy_name: &'static str,
88) -> Result<reqwest::Client, ProxyLifecycleError> {
89 build_pooled_client().map_err(|error| ProxyLifecycleError::Io {
90 message: format!("failed to create {proxy_name} HTTP client: {error}"),
91 })
92}
93
94pub(crate) async fn serve_router(
96 proxy_name: &'static str,
97 host: &str,
98 port: u16,
99 router: axum::Router,
100) -> Result<(), ProxyLifecycleError> {
101 let listener = tokio::net::TcpListener::bind((host, port))
102 .await
103 .map_err(|error| ProxyLifecycleError::Io {
104 message: format!("failed to bind {proxy_name} on {host}:{port}: {error}"),
105 })?;
106 axum::serve(listener, router)
107 .await
108 .map_err(|error| ProxyLifecycleError::Io {
109 message: format!("{proxy_name} server failed: {error}"),
110 })
111}
112
113pub(crate) async fn await_backends(client: reqwest::Client, urls: Vec<String>, path: &'static str) {
115 let waits = urls
116 .into_iter()
117 .map(|url| await_backend(client.clone(), url, path));
118 futures_util::future::join_all(waits).await;
119}
120
121async fn await_backend(client: reqwest::Client, url: String, path: &'static str) {
122 loop {
123 if client
124 .get(join_path(&url, path))
125 .send()
126 .await
127 .is_ok_and(|response| response.status().is_success())
128 {
129 return;
130 }
131 tokio::time::sleep(Duration::from_secs(1)).await;
132 }
133}
134
135pub(crate) fn fanout_target_urls<'a>(
138 prefill_urls: impl IntoIterator<Item = &'a str>,
139 decode_urls: impl IntoIterator<Item = &'a str>,
140) -> Vec<String> {
141 prefill_urls
142 .into_iter()
143 .chain(decode_urls)
144 .map(str::to_owned)
145 .collect()
146}
147
148pub(crate) fn upstream_response_builder(
151 response: &reqwest::Response,
152) -> Result<axum::http::response::Builder, ProxyHttpError> {
153 let mut builder = Response::builder().status(status_code(response.status())?);
154 if let Some(content_type) = response
155 .headers()
156 .get(reqwest::header::CONTENT_TYPE)
157 .and_then(|value| value.to_str().ok())
158 {
159 builder = builder.header(header::CONTENT_TYPE, content_type);
160 }
161 Ok(builder)
162}
163
164pub(crate) fn response_body(
166 builder: axum::http::response::Builder,
167 body: Body,
168) -> Result<Response<Body>, ProxyHttpError> {
169 builder.body(body).map_err(|error| {
170 ProxyHttpError::internal(format!("failed to build proxy response: {error}"))
171 })
172}
173
174pub async fn forward_response(
177 response: reqwest::Response,
178) -> Result<Response<Body>, ProxyHttpError> {
179 let builder = upstream_response_builder(&response)?;
180 let bytes = response
181 .bytes()
182 .await
183 .map_err(|error| ProxyHttpError::upstream("upstream response body read failed", error))?;
184 response_body(builder, Body::from(bytes))
185}
186
187pub async fn upstream_status_error(context: &str, response: reqwest::Response) -> ProxyHttpError {
190 let status = response.status();
191 let body = match response.text().await {
192 Ok(text) => text,
193 Err(error) => format!("<failed to read upstream error body: {error}>"),
194 };
195 ProxyHttpError::status(
196 StatusCode::BAD_GATEWAY,
197 format!("{context} returned HTTP {status}: {body}"),
198 )
199}
200
201pub fn outbound_authorization(headers: &HeaderMap) -> Option<String> {
204 headers
205 .get(header::AUTHORIZATION)
206 .and_then(|value| value.to_str().ok())
207 .map(str::to_owned)
208 .or_else(|| {
209 env::var("OPENAI_API_KEY")
210 .ok()
211 .map(|key| format!("Bearer {key}"))
212 })
213}
214
215pub fn join_path(base: &str, path: &str) -> String {
218 format!("{}{}", base.trim_end_matches('/'), path)
219}
220
221pub fn status_code(status: reqwest::StatusCode) -> Result<StatusCode, ProxyHttpError> {
223 StatusCode::from_u16(status.as_u16())
224 .map_err(|error| ProxyHttpError::internal(format!("invalid upstream status code: {error}")))
225}
226
227pub(crate) fn round_robin_index(cursor: &AtomicUsize, len: usize) -> usize {
231 cursor.fetch_add(1, Ordering::SeqCst) % len
232}
233
234pub(crate) async fn send_json_post(
240 client: reqwest::Client,
241 url: String,
242 body: &Value,
243 request_id: Option<&str>,
244 authorization: Option<&str>,
245 extra_headers: &[(&str, String)],
246 context: &'static str,
247) -> Result<reqwest::Response, ProxyHttpError> {
248 let response = send_json_post_status(
249 client,
250 url,
251 body,
252 request_id,
253 authorization,
254 extra_headers,
255 context,
256 )
257 .await?;
258 if !response.status().is_success() {
259 return Err(upstream_status_error(context, response).await);
260 }
261 Ok(response)
262}
263
264pub(crate) async fn send_json_post_status(
268 client: reqwest::Client,
269 url: String,
270 body: &Value,
271 request_id: Option<&str>,
272 authorization: Option<&str>,
273 extra_headers: &[(&str, String)],
274 context: &'static str,
275) -> Result<reqwest::Response, ProxyHttpError> {
276 let mut request = client.post(url).json(body);
277 if let Some(request_id) = request_id {
278 request = request.header("X-Request-Id", request_id);
279 }
280 for (name, value) in extra_headers {
283 request = request.header(*name, value);
284 }
285 if let Some(authorization) = authorization {
286 request = request.header(reqwest::header::AUTHORIZATION, authorization);
287 }
288 request
289 .send()
290 .await
291 .map_err(|error| ProxyHttpError::upstream(&format!("{context} failed"), error))
292}
293
294pub(crate) fn next_request_id(counter: &AtomicUsize) -> String {
297 let value = counter.fetch_add(1, Ordering::SeqCst);
298 format!("{}-{value}", std::process::id())
299}
300
301pub(crate) fn build_pooled_client() -> reqwest::Result<reqwest::Client> {
305 reqwest::Client::builder()
306 .pool_max_idle_per_host(usize::MAX)
307 .build()
308}
309
310pub(crate) const FANOUT_TARGET_TIMEOUT: Duration = Duration::from_secs(60);
316
317#[derive(Debug, Deserialize, Serialize)]
319pub struct FanoutFailure {
320 pub url: String,
321 pub error: String,
322}
323
324#[derive(Debug, Deserialize, Serialize)]
328pub struct ResetPrefixCacheResponse {
329 pub successful: Vec<String>,
330 pub failed: Vec<FanoutFailure>,
331}
332
333#[derive(Debug, Deserialize, Serialize)]
337pub struct PrimePrefixCacheResponse {
338 pub targets: Vec<PrimePrefixCacheTarget>,
339}
340
341#[derive(Debug, Deserialize, Serialize)]
344pub struct PrimePrefixCacheTarget {
345 pub url: String,
346 pub rank: u32,
347 pub http_status: Option<u16>,
348 pub elapsed_ms: u64,
349 pub error: Option<String>,
350}
351
352pub(crate) struct PrimeFlowFailure {
355 pub http_status: Option<u16>,
356 pub error: String,
357}
358
359impl PrimeFlowFailure {
360 pub(crate) fn transport(error: ProxyHttpError) -> Self {
361 Self {
362 http_status: None,
363 error: error.to_string(),
364 }
365 }
366
367 pub(crate) fn status(status: u16, detail: String) -> Self {
368 Self {
369 http_status: Some(status),
370 error: detail,
371 }
372 }
373}
374
375pub(crate) async fn expect_2xx(
380 context: &'static str,
381 response: reqwest::Response,
382) -> Result<(u16, String), PrimeFlowFailure> {
383 let status = response.status().as_u16();
384 let text = response.text().await.map_err(|error| {
385 PrimeFlowFailure::transport(ProxyHttpError::upstream(
386 &format!("{context} response read failed"),
387 error,
388 ))
389 })?;
390 if !(200..300).contains(&status) {
391 return Err(PrimeFlowFailure::status(
392 status,
393 format!("{context} returned HTTP {status}: {text}"),
394 ));
395 }
396 Ok((status, text))
397}
398
399pub(crate) trait PrimeFanoutTarget {
402 fn url(&self) -> &str;
403 fn rank(&self) -> u32;
404}
405
406pub(crate) trait PrimeReplica {
409 fn url(&self) -> &str;
410 fn data_parallel_size(&self) -> u32;
411}
412
413pub(crate) struct RankedPrimeTarget<R> {
416 pub replica: R,
417 pub rank: u32,
418}
419
420impl<R: PrimeReplica> PrimeFanoutTarget for RankedPrimeTarget<R> {
421 fn url(&self) -> &str {
422 self.replica.url()
423 }
424
425 fn rank(&self) -> u32 {
426 self.rank
427 }
428}
429
430pub(crate) fn ranked_prime_targets<R: PrimeReplica + Clone>(
433 replicas: &[R],
434) -> Vec<RankedPrimeTarget<R>> {
435 let mut targets = Vec::new();
436 for replica in replicas {
437 for rank in 0..replica.data_parallel_size().max(1) {
438 targets.push(RankedPrimeTarget {
439 replica: replica.clone(),
440 rank,
441 });
442 }
443 }
444 targets
445}
446
447pub(crate) async fn run_sweep_fanout(
453 client: reqwest::Client,
454 operation: &'static str,
455 path: &'static str,
456 targets: Vec<String>,
457 authorization: Option<String>,
458) -> Response<Body> {
459 if targets.is_empty() {
460 return empty_fanout_failure(operation);
461 }
462 let attempts = targets
463 .into_iter()
464 .map(|url| sweep_target(client.clone(), operation, path, url, authorization.clone()));
465 let mut successful = Vec::new();
466 let mut failed = Vec::new();
467 for result in futures_util::future::join_all(attempts).await {
468 match result {
469 Ok(url) => successful.push(url),
470 Err(failure) => failed.push(failure),
471 }
472 }
473 let status = if failed.is_empty() {
474 StatusCode::OK
475 } else {
476 StatusCode::PARTIAL_CONTENT
477 };
478 (
479 status,
480 Json(ResetPrefixCacheResponse { successful, failed }),
481 )
482 .into_response()
483}
484
485async fn sweep_target(
486 client: reqwest::Client,
487 operation: &'static str,
488 path: &'static str,
489 url: String,
490 authorization: Option<String>,
491) -> Result<String, FanoutFailure> {
492 let endpoint = join_path(&url, path);
493 let mut request = client.post(endpoint).timeout(FANOUT_TARGET_TIMEOUT);
494 if let Some(authorization) = authorization {
495 request = request.header(reqwest::header::AUTHORIZATION, authorization);
496 }
497 let response = request.send().await.map_err(|error| FanoutFailure {
498 url: url.clone(),
499 error: format!("{operation} request failed: {error}"),
500 })?;
501 if response.status().is_success() && response.status() != reqwest::StatusCode::PARTIAL_CONTENT {
504 Ok(url)
505 } else {
506 let status = response.status();
507 let detail = response
508 .text()
509 .await
510 .unwrap_or_else(|error| format!("failed to read response body: {error}"));
511 Err(FanoutFailure {
512 url,
513 error: format!("HTTP {status}: {detail}"),
514 })
515 }
516}
517
518pub(crate) async fn run_prime_fanout<T, F, Fut>(
525 operation: &'static str,
526 targets: Vec<T>,
527 execute: F,
528) -> Response<Body>
529where
530 T: PrimeFanoutTarget,
531 F: FnMut(T) -> Fut,
532 Fut: Future<Output = Result<u16, PrimeFlowFailure>>,
533{
534 run_prime_fanout_with_timeout(operation, targets, execute, FANOUT_TARGET_TIMEOUT).await
535}
536
537async fn run_prime_fanout_with_timeout<T, F, Fut>(
538 operation: &'static str,
539 targets: Vec<T>,
540 mut execute: F,
541 target_timeout: Duration,
542) -> Response<Body>
543where
544 T: PrimeFanoutTarget,
545 F: FnMut(T) -> Fut,
546 Fut: Future<Output = Result<u16, PrimeFlowFailure>>,
547{
548 if targets.is_empty() {
549 return empty_fanout_failure(operation);
550 }
551 let mut results = Vec::new();
552 for target in targets {
553 let url = target.url().to_owned();
554 let rank = target.rank();
555 let started = std::time::Instant::now();
556 let outcome = tokio::time::timeout(target_timeout, execute(target)).await;
557 let elapsed_ms = u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX);
558 results.push(match outcome {
559 Ok(Ok(status)) => PrimePrefixCacheTarget {
560 url,
561 rank,
562 http_status: Some(status),
563 elapsed_ms,
564 error: None,
565 },
566 Ok(Err(failure)) => PrimePrefixCacheTarget {
567 url,
568 rank,
569 http_status: failure.http_status,
570 elapsed_ms,
571 error: Some(failure.error),
572 },
573 Err(_elapsed) => PrimePrefixCacheTarget {
574 url,
575 rank,
576 http_status: None,
577 elapsed_ms,
578 error: Some(format!(
579 "{operation} timed out after {}s",
580 target_timeout.as_secs()
581 )),
582 },
583 });
584 }
585 let status = if results.iter().all(|target| target.error.is_none()) {
586 StatusCode::OK
587 } else {
588 StatusCode::PARTIAL_CONTENT
589 };
590 (status, Json(PrimePrefixCacheResponse { targets: results })).into_response()
591}
592
593fn empty_fanout_failure(operation: &str) -> Response<Body> {
596 ProxyHttpError::status(
597 StatusCode::BAD_GATEWAY,
598 format!("{operation} fan-out has no targets: no prefill replica or data-parallel rank is available"),
599 )
600 .into_response()
601}
602
603#[derive(Debug)]
605pub struct ProxyHttpError {
606 status: StatusCode,
607 message: String,
608}
609
610impl ProxyHttpError {
611 pub fn status(status: StatusCode, message: impl Into<String>) -> Self {
612 Self {
613 status,
614 message: message.into(),
615 }
616 }
617
618 pub fn upstream(context: &str, error: reqwest::Error) -> Self {
619 Self::status(StatusCode::BAD_GATEWAY, format!("{context}: {error}"))
620 }
621
622 pub fn internal(message: impl Into<String>) -> Self {
623 Self::status(StatusCode::INTERNAL_SERVER_ERROR, message)
624 }
625}
626
627impl fmt::Display for ProxyHttpError {
628 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
629 write!(formatter, "{}", self.message)
630 }
631}
632
633impl std::error::Error for ProxyHttpError {}
634
635impl IntoResponse for ProxyHttpError {
636 fn into_response(self) -> axum::response::Response {
637 let body = Json(ProxyErrorResponse {
638 error: self.message,
639 });
640 (self.status, body).into_response()
641 }
642}
643
644#[derive(Serialize)]
645pub struct ProxyErrorResponse {
646 pub error: String,
647}
648
649#[derive(Clone, Copy, Debug)]
652pub(crate) enum OnClientDrop {
653 Abort,
656 Detach,
662}
663
664pub(crate) fn stream_decode_response(
672 response: reqwest::Response,
673 prefill_task: JoinHandle<Result<(), ProxyHttpError>>,
674 on_client_drop: OnClientDrop,
675) -> Result<Response<Body>, ProxyHttpError> {
676 let builder = upstream_response_builder(&response)?;
677 let stream = decode_response_stream(response.bytes_stream(), prefill_task, on_client_drop);
678 response_body(builder, Body::from_stream(stream))
679}
680
681pub(crate) fn stream_response(
683 response: reqwest::Response,
684) -> Result<Response<Body>, ProxyHttpError> {
685 let builder = upstream_response_builder(&response)?;
686 let stream = response
687 .bytes_stream()
688 .map(|chunk| chunk.map_err(|error| stream_error(format!("decode stream failed: {error}"))));
689 response_body(builder, Body::from_stream(stream))
690}
691
692pub(crate) fn decode_response_stream<S, E>(
699 decode_stream: S,
700 prefill_task: JoinHandle<Result<(), ProxyHttpError>>,
701 on_client_drop: OnClientDrop,
702) -> impl Stream<Item = std::result::Result<Bytes, std::io::Error>>
703where
704 S: Stream<Item = std::result::Result<Bytes, E>> + Unpin,
705 E: fmt::Display,
706{
707 let prefill_abort = prefill_task.abort_handle();
708 try_stream! {
709 let mut decode_stream = decode_stream;
710 let mut prefill_task = prefill_task;
711 let mut prefill_abort = match on_client_drop {
715 OnClientDrop::Abort => Some(AbortOnDrop::new(prefill_abort)),
716 OnClientDrop::Detach => None,
717 };
718 let mut prefill_done = false;
719 loop {
720 match next_stream_event(&mut prefill_task, &mut decode_stream, prefill_done).await {
721 StreamEvent::Prefill(prefill) => {
722 prefill_done = true;
723 match decode_stream.next().now_or_never() {
732 Some(Some(Ok(bytes))) => yield bytes,
733 Some(Some(Err(error))) => {
734 Err(stream_error(format!("decode stream failed: {error}")))?;
735 }
736 Some(None) | None => {}
737 }
738 prefill
739 .map_err(join_error)?
740 .map_err(|error| stream_error(error.to_string()))?;
741 if let Some(abort) = &mut prefill_abort {
742 abort.disarm();
743 }
744 }
745 StreamEvent::Decode(Some(Ok(bytes))) => yield bytes,
746 StreamEvent::Decode(Some(Err(error))) => {
747 Err(stream_error(format!("decode stream failed: {error}")))?;
748 }
749 StreamEvent::Decode(None) => break,
750 }
751 }
752 if !prefill_done {
753 prefill_task
754 .await
755 .map_err(join_error)?
756 .map_err(|error| stream_error(error.to_string()))?;
757 if let Some(abort) = &mut prefill_abort {
758 abort.disarm();
759 }
760 }
761 }
762}
763
764enum StreamEvent<E> {
765 Prefill(std::result::Result<Result<(), ProxyHttpError>, tokio::task::JoinError>),
766 Decode(Option<std::result::Result<Bytes, E>>),
767}
768
769async fn next_stream_event<S, E>(
770 prefill_task: &mut JoinHandle<Result<(), ProxyHttpError>>,
771 decode_stream: &mut S,
772 prefill_done: bool,
773) -> StreamEvent<E>
774where
775 S: Stream<Item = std::result::Result<Bytes, E>> + Unpin,
776{
777 if !prefill_done && prefill_task.is_finished() {
784 return StreamEvent::Prefill(prefill_task.await);
785 }
786 tokio::select! {
792 prefill = prefill_task, if !prefill_done => StreamEvent::Prefill(prefill),
793 chunk = decode_stream.next() => StreamEvent::Decode(chunk),
794 }
795}
796
797fn join_error(error: tokio::task::JoinError) -> std::io::Error {
798 stream_error(format!("prefill task failed: {error}"))
799}
800
801fn stream_error(message: String) -> std::io::Error {
802 std::io::Error::other(message)
803}
804
805struct AbortOnDrop {
807 handle: tokio::task::AbortHandle,
808 armed: bool,
809}
810
811impl AbortOnDrop {
812 fn new(handle: tokio::task::AbortHandle) -> Self {
813 Self {
814 handle,
815 armed: true,
816 }
817 }
818
819 fn disarm(&mut self) {
820 self.armed = false;
821 }
822}
823
824impl Drop for AbortOnDrop {
825 fn drop(&mut self) {
826 if self.armed {
827 self.handle.abort();
828 }
829 }
830}
831
832#[cfg(test)]
833mod tests {
834 use super::*;
835 use anyhow::{Context, Result};
836
837 #[test]
838 fn join_path_normalizes_single_trailing_slash() {
839 assert_eq!(
840 join_path("http://h:1/", "/v1/models"),
841 "http://h:1/v1/models"
842 );
843 assert_eq!(
844 join_path("http://h:1", "/v1/models"),
845 "http://h:1/v1/models"
846 );
847 }
848
849 #[test]
850 fn status_code_maps_reqwest_status() -> Result<()> {
851 let mapped = status_code(reqwest::StatusCode::OK)
852 .map_err(|error| anyhow::anyhow!(error.to_string()))?;
853 assert_eq!(mapped, StatusCode::OK);
854 Ok(())
855 }
856
857 #[test]
858 fn outbound_authorization_prefers_inbound_header() -> Result<()> {
859 let mut headers = HeaderMap::new();
860 headers.insert(header::AUTHORIZATION, "Bearer inbound".parse()?);
861 assert_eq!(
862 outbound_authorization(&headers),
863 Some("Bearer inbound".to_owned())
864 );
865 Ok(())
866 }
867
868 #[test]
869 fn proxy_error_internal_uses_500() {
870 let error = ProxyHttpError::internal("boom");
871 assert_eq!(error.status, StatusCode::INTERNAL_SERVER_ERROR);
872 assert_eq!(error.to_string(), "boom");
873 }
874
875 struct StaticPrimeTarget {
876 url: &'static str,
877 rank: u32,
878 }
879
880 impl PrimeFanoutTarget for StaticPrimeTarget {
881 fn url(&self) -> &str {
882 self.url
883 }
884
885 fn rank(&self) -> u32 {
886 self.rank
887 }
888 }
889
890 #[test]
893 fn prime_fanout_rejects_an_empty_target_set() -> Result<()> {
894 let runtime = proxy_test_runtime()?;
895 let response = runtime.block_on(run_prime_fanout(
896 "prefix cache conditioning",
897 Vec::<StaticPrimeTarget>::new(),
898 |_target| async { Ok::<u16, PrimeFlowFailure>(200) },
899 ));
900 assert_eq!(response.status(), StatusCode::BAD_GATEWAY);
901 let body = runtime.block_on(axum::body::to_bytes(response.into_body(), usize::MAX))?;
902 let value: Value = serde_json::from_slice(&body)?;
903 assert!(
904 value["error"]
905 .as_str()
906 .is_some_and(|error| error.contains("no targets")),
907 "got {value}"
908 );
909 Ok(())
910 }
911
912 #[test]
915 fn sweep_fanout_rejects_an_empty_target_set() -> Result<()> {
916 let runtime = proxy_test_runtime()?;
917 let client = build_pooled_client().map_err(|error| anyhow::anyhow!(error.to_string()))?;
918 let response = runtime.block_on(run_sweep_fanout(
919 client,
920 "prefix cache reset",
921 "/reset_prefix_cache",
922 Vec::new(),
923 None,
924 ));
925 assert_eq!(response.status(), StatusCode::BAD_GATEWAY);
926 let body = runtime.block_on(axum::body::to_bytes(response.into_body(), usize::MAX))?;
927 let value: Value = serde_json::from_slice(&body)?;
928 assert!(
929 value["error"]
930 .as_str()
931 .is_some_and(|error| error.contains("no targets")),
932 "got {value}"
933 );
934 Ok(())
935 }
936
937 #[test]
940 fn prime_fanout_times_out_a_hung_target() -> Result<()> {
941 let runtime = proxy_test_runtime()?;
942 let response = runtime.block_on(run_prime_fanout_with_timeout(
943 "prefix cache conditioning",
944 vec![StaticPrimeTarget {
945 url: "http://127.0.0.1:1",
946 rank: 0,
947 }],
948 |_target| async {
949 futures_util::future::pending::<()>().await;
950 Ok::<u16, PrimeFlowFailure>(200)
951 },
952 Duration::from_millis(50),
953 ));
954 assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
955 let body = runtime.block_on(axum::body::to_bytes(response.into_body(), usize::MAX))?;
956 let value: Value = serde_json::from_slice(&body)?;
957 assert_eq!(value["targets"][0]["http_status"], Value::Null);
958 assert!(
959 value["targets"][0]["error"]
960 .as_str()
961 .is_some_and(|error| error.contains("timed out")),
962 "got {value}"
963 );
964 Ok(())
965 }
966
967 use std::sync::Arc;
968 use std::sync::atomic::{AtomicBool, Ordering};
969
970 struct SetOnDrop(Arc<AtomicBool>);
973
974 impl Drop for SetOnDrop {
975 fn drop(&mut self) {
976 self.0.store(true, Ordering::SeqCst);
977 }
978 }
979
980 fn proxy_test_runtime() -> Result<tokio::runtime::Runtime> {
981 tokio::runtime::Builder::new_multi_thread()
982 .enable_all()
983 .build()
984 .map_err(|error| anyhow::anyhow!(error.to_string()))
985 }
986
987 #[test]
988 fn streamed_decode_yields_bytes_in_order_when_prefill_succeeds() -> Result<()> {
989 let runtime = proxy_test_runtime()?;
990 let bytes = runtime.block_on(async {
991 let decode = Box::pin(futures_util::stream::iter(vec![
992 std::result::Result::<Bytes, std::io::Error>::Ok(Bytes::from_static(b"hello")),
993 Ok(Bytes::from_static(b" world")),
994 ]));
995 let prefill = tokio::spawn(async { Ok::<(), ProxyHttpError>(()) });
996 let mut stream = Box::pin(decode_response_stream(decode, prefill, OnClientDrop::Abort));
997 let mut out = Vec::new();
998 while let Some(item) = stream.next().await {
999 out.push(item.map_err(|error| anyhow::anyhow!(error.to_string()))?);
1000 }
1001 anyhow::Ok(out)
1002 })?;
1003 let joined: Vec<u8> = bytes.into_iter().flatten().collect();
1004 assert_eq!(joined, b"hello world");
1005 Ok(())
1006 }
1007
1008 #[test]
1009 fn streamed_decode_surfaces_prefill_error_after_decode_ends() -> Result<()> {
1010 let runtime = proxy_test_runtime()?;
1011 let (bytes, error) = runtime.block_on(async {
1012 let decode = Box::pin(futures_util::stream::iter(vec![std::result::Result::<
1013 Bytes,
1014 std::io::Error,
1015 >::Ok(
1016 Bytes::from_static(b"partial"),
1017 )]));
1018 let prefill = tokio::spawn(async {
1019 Err::<(), ProxyHttpError>(ProxyHttpError::internal("prefill boom"))
1020 });
1021 let mut stream = Box::pin(decode_response_stream(decode, prefill, OnClientDrop::Abort));
1022 let mut bytes = Vec::new();
1023 let mut error = None;
1024 while let Some(item) = stream.next().await {
1025 match item {
1026 Ok(chunk) => bytes.extend_from_slice(&chunk),
1027 Err(stream_error) => {
1028 error = Some(stream_error.to_string());
1029 break;
1030 }
1031 }
1032 }
1033 anyhow::Ok((bytes, error))
1034 })?;
1035 assert_eq!(bytes, b"partial");
1036 let error = error.context("expected a prefill error to surface after decode ended")?;
1037 assert!(error.contains("prefill boom"), "got {error}");
1038 Ok(())
1039 }
1040
1041 #[test]
1046 fn prefill_error_surfaces_even_while_decode_stays_ready() -> Result<()> {
1047 let runtime = proxy_test_runtime()?;
1048 let error = runtime.block_on(async {
1049 let decode = Box::pin(futures_util::stream::repeat_with(|| {
1051 std::result::Result::<Bytes, std::io::Error>::Ok(Bytes::from_static(b"x"))
1052 }));
1053 let prefill = tokio::spawn(async {
1054 Err::<(), ProxyHttpError>(ProxyHttpError::internal("prefill boom"))
1055 });
1056 let mut stream = Box::pin(decode_response_stream(decode, prefill, OnClientDrop::Abort));
1057 let mut chunks = 0usize;
1058 let mut error = None;
1059 while let Some(item) = stream.next().await {
1060 match item {
1061 Ok(_) => {
1062 chunks += 1;
1063 assert!(
1067 chunks < 100_000,
1068 "prefill error was suppressed by a continuously-ready decode stream"
1069 );
1070 }
1071 Err(stream_error) => {
1072 error = Some(stream_error.to_string());
1073 break;
1074 }
1075 }
1076 }
1077 anyhow::Ok(error)
1078 })?;
1079 let error = error.context("a prefill error must surface even while decode stays ready")?;
1080 assert!(error.contains("prefill boom"), "got {error}");
1081 Ok(())
1082 }
1083
1084 #[test]
1090 fn decode_error_ready_at_tiebreak_is_not_swallowed() -> Result<()> {
1091 let runtime = proxy_test_runtime()?;
1092 let error = runtime.block_on(async {
1093 let prefill = tokio::spawn(async { Ok::<(), ProxyHttpError>(()) });
1096 while !prefill.is_finished() {
1097 tokio::task::yield_now().await;
1098 }
1099 let decode = Box::pin(futures_util::stream::iter(vec![std::result::Result::<
1101 Bytes,
1102 std::io::Error,
1103 >::Err(
1104 std::io::Error::other("decode boom"),
1105 )]));
1106 let mut stream = Box::pin(decode_response_stream(decode, prefill, OnClientDrop::Abort));
1107 let mut error = None;
1108 while let Some(item) = stream.next().await {
1109 if let Err(stream_error) = item {
1110 error = Some(stream_error.to_string());
1111 break;
1112 }
1113 }
1114 anyhow::Ok(error)
1115 })?;
1116 let error =
1117 error.context("a decode error ready at the tie-break must surface, not truncate")?;
1118 assert!(error.contains("decode boom"), "got {error}");
1119 Ok(())
1120 }
1121
1122 #[test]
1123 fn dropping_the_stream_before_prefill_finishes_aborts_prefill() -> Result<()> {
1124 let runtime = proxy_test_runtime()?;
1125 let aborted = Arc::new(AtomicBool::new(false));
1126 let flag = aborted.clone();
1127 let cancelled = runtime.block_on(async move {
1128 let (started_tx, started_rx) = tokio::sync::oneshot::channel::<()>();
1129 let prefill = tokio::spawn(async move {
1134 let _guard = SetOnDrop(flag);
1135 let _ = started_tx.send(());
1136 futures_util::future::pending::<()>().await;
1137 Ok::<(), ProxyHttpError>(())
1138 });
1139 let _ = started_rx.await;
1140 let decode = Box::pin(
1143 futures_util::stream::once(async {
1144 std::result::Result::<Bytes, std::io::Error>::Ok(Bytes::from_static(b"a"))
1145 })
1146 .chain(futures_util::stream::pending::<
1147 std::result::Result<Bytes, std::io::Error>,
1148 >()),
1149 );
1150 let mut stream = Box::pin(decode_response_stream(decode, prefill, OnClientDrop::Abort));
1151 assert!(matches!(stream.next().await, Some(Ok(_))));
1152 drop(stream);
1153 for _ in 0..200 {
1154 if aborted.load(Ordering::SeqCst) {
1155 return true;
1156 }
1157 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1158 }
1159 false
1160 });
1161 assert!(
1162 cancelled,
1163 "prefill task was not aborted when the response stream was dropped"
1164 );
1165 Ok(())
1166 }
1167
1168 #[test]
1172 fn dropping_the_stream_before_prefill_finishes_detaches_prefill() -> Result<()> {
1173 let runtime = proxy_test_runtime()?;
1174 let completed = Arc::new(AtomicBool::new(false));
1175 let flag = completed.clone();
1176 let finished = runtime.block_on(async move {
1177 let (started_tx, started_rx) = tokio::sync::oneshot::channel::<()>();
1178 let prefill = tokio::spawn(async move {
1181 let _ = started_tx.send(());
1182 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1183 flag.store(true, Ordering::SeqCst);
1184 Ok::<(), ProxyHttpError>(())
1185 });
1186 let _ = started_rx.await;
1187 let decode = Box::pin(
1190 futures_util::stream::once(async {
1191 std::result::Result::<Bytes, std::io::Error>::Ok(Bytes::from_static(b"a"))
1192 })
1193 .chain(futures_util::stream::pending::<
1194 std::result::Result<Bytes, std::io::Error>,
1195 >()),
1196 );
1197 let mut stream = Box::pin(decode_response_stream(
1198 decode,
1199 prefill,
1200 OnClientDrop::Detach,
1201 ));
1202 assert!(matches!(stream.next().await, Some(Ok(_))));
1203 drop(stream);
1204 for _ in 0..200 {
1205 if completed.load(Ordering::SeqCst) {
1206 return true;
1207 }
1208 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1209 }
1210 false
1211 });
1212 assert!(
1213 finished,
1214 "prefill task was aborted instead of detached when the response stream was dropped"
1215 );
1216 Ok(())
1217 }
1218}