Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 95 additions & 5 deletions crates/trusted-server-core/src/auction/orchestrator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,28 @@ use super::config::AuctionConfig;
use super::provider::AuctionProvider;
use super::types::{AuctionContext, AuctionRequest, AuctionResponse, Bid, BidStatus};

const PROVIDER_ERROR_MESSAGE_CHARS: usize = 500;

fn provider_error_message(error: &Report<TrustedServerError>) -> String {
error
.current_context()
.to_string()
.chars()
.take(PROVIDER_ERROR_MESSAGE_CHARS)
.collect()
}
Comment thread
ChristianPavilonis marked this conversation as resolved.

fn provider_error_response(
provider_name: &str,
response_time_ms: u64,
error_type: &str,
error: &Report<TrustedServerError>,
) -> AuctionResponse {
AuctionResponse::error(provider_name, response_time_ms)
.with_metadata("error_type", serde_json::json!(error_type))
.with_metadata("message", serde_json::json!(provider_error_message(error)))
}

/// Compute the remaining time budget from a deadline.
///
/// Returns the number of milliseconds left before `timeout_ms` is exceeded,
Expand Down Expand Up @@ -274,6 +296,7 @@ impl AuctionOrchestrator {
let mut backend_to_provider: HashMap<String, (&str, Instant, &dyn AuctionProvider)> =
HashMap::new();
let mut pending_requests: Vec<PlatformPendingRequest> = Vec::new();
let mut responses = Vec::new();
Comment thread
ChristianPavilonis marked this conversation as resolved.

for provider_name in provider_names {
let provider = match self.providers.get(provider_name) {
Expand Down Expand Up @@ -353,11 +376,18 @@ impl AuctionOrchestrator {
);
}
Err(e) => {
let response_time_ms = start_time.elapsed().as_millis() as u64;
log::warn!(
"Provider '{}' failed to launch request: {:?}",
provider.provider_name(),
e
);
responses.push(provider_error_response(
provider.provider_name(),
response_time_ms,
"launch_failed",
&e,
));
}
}
}
Expand All @@ -377,7 +407,6 @@ impl AuctionOrchestrator {
// transport timeout fires). Hard deadline enforcement therefore depends
// on every backend's `first_byte_timeout` being set to at most the
// remaining auction budget — which Phase 1 above guarantees.
let mut responses = Vec::new();
let mut remaining = pending_requests;

while !remaining.is_empty() {
Expand Down Expand Up @@ -419,9 +448,11 @@ impl AuctionOrchestrator {
provider_name,
e
);
responses.push(AuctionResponse::error(
responses.push(provider_error_response(
provider_name,
response_time_ms,
"parse_response",
&e,
));
}
}
Expand All @@ -432,8 +463,12 @@ impl AuctionOrchestrator {
provider_name,
e
);
responses
.push(AuctionResponse::error(provider_name, response_time_ms));
responses.push(provider_error_response(
provider_name,
response_time_ms,
"unsupported_response_body",
&e,
));
}
}
} else {
Expand Down Expand Up @@ -635,8 +670,9 @@ mod tests {
use crate::auction::config::AuctionConfig;
use crate::auction::test_support::create_test_auction_context;
use crate::auction::types::{
AdFormat, AdSlot, AuctionRequest, Bid, MediaType, PublisherInfo, UserInfo,
AdFormat, AdSlot, AuctionRequest, Bid, BidStatus, MediaType, PublisherInfo, UserInfo,
};
use crate::error::TrustedServerError;

// All-None ClientInfo used across tests that don't need real IP/TLS data.
// Defined as a const so &EMPTY_CLIENT_INFO has 'static lifetime, avoiding
Expand All @@ -648,6 +684,7 @@ mod tests {
};
use crate::platform::test_support::noop_services;
use crate::test_support::tests::crate_test_settings_str;
use error_stack::Report;
use fastly::Request;
use std::collections::{HashMap, HashSet};

Expand Down Expand Up @@ -700,6 +737,59 @@ mod tests {
crate::settings::Settings::from_toml(&settings_str).expect("should parse test settings")
}

#[test]
fn provider_error_response_includes_diagnostic_metadata() {
let error = Report::new(TrustedServerError::Auction {
message: "parse failed".to_string(),
})
.attach("internal/source.rs:12:34");

let response = super::provider_error_response("prebid", 37, "parse_response", &error);

assert_eq!(
response.status,
BidStatus::Error,
"should mark diagnostic provider responses as errors"
);
assert_eq!(
response.metadata["error_type"],
serde_json::json!("parse_response"),
"should include the provider error classification"
);

let message = response.metadata["message"]
.as_str()
.expect("should include provider error message");
assert!(
message.contains("parse failed"),
"should include user-safe diagnostic detail"
);
assert!(
!message.contains("internal/source.rs"),
"should not include attached internal details"
);
}

#[test]
fn provider_error_message_truncates_user_safe_context() {
let long_message = "x".repeat(super::PROVIDER_ERROR_MESSAGE_CHARS + 100);
let error = Report::new(TrustedServerError::Auction {
message: long_message,
});

let message = super::provider_error_message(&error);

assert_eq!(
message.chars().count(),
super::PROVIDER_ERROR_MESSAGE_CHARS,
"should cap provider error messages"
);
assert!(
message.starts_with("Auction error: "),
"should preserve the current context display text"
);
}

#[test]
fn filters_winning_bids_below_floor() {
let orchestrator = AuctionOrchestrator::new(AuctionConfig::default());
Expand Down
Loading
Loading