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
11 changes: 2 additions & 9 deletions crates/trusted-server-adapter-fastly/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,20 +180,13 @@ async fn route_request(
handle_first_party_proxy_rebuild(settings, runtime_services, req).await
}
(m, path) if integration_registry.has_route(&m, path) => integration_registry
.handle_proxy(
&m,
path,
settings,
runtime_services,
compat::to_fastly_request(req),
)
.handle_proxy(&m, path, settings, runtime_services, req)
.await
.unwrap_or_else(|| {
Err(Report::new(TrustedServerError::BadRequest {
message: format!("Unknown integration route: {path}"),
}))
})
.map(compat::from_fastly_response),
}),

// No known route matched, proxy to publisher origin as fallback
_ => {
Expand Down
24 changes: 17 additions & 7 deletions crates/trusted-server-core/src/auction/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ The auction orchestration system allows you to:
┌─────────────────────────────────────────────────────────┐
│ AuctionProvider Trait │
│ - request_bids() │
│ - request_bids() async │
│ - parse_response() │
│ - provider_name() │
│ - timeout_ms() │
│ - is_enabled() │
Expand Down Expand Up @@ -54,7 +55,7 @@ When a request arrives at the `/auction` endpoint, it goes through the following
┌──────────────────────────────────────────────────────────────────────┐
│ 2. Route Matching (crates/trusted-server-adapter-fastly/src/main.rs:84) │
│ - Pattern: (Method::POST, "/auction") │
│ - Handler: handle_auction(settings, &orchestrator, &storage, req)│
│ - Handler: handle_auction(settings, &orchestrator, runtime_services, req)│
└──────────────────────────────────────────────────────────────────────┘
Expand Down Expand Up @@ -496,6 +497,7 @@ timeout_ms = 500
use async_trait::async_trait;
use crate::auction::provider::AuctionProvider;
use crate::auction::types::{AuctionContext, AuctionRequest, AuctionResponse};
use crate::platform::{PlatformPendingRequest, PlatformResponse};

pub struct YourAuctionProvider {
config: YourConfig,
Expand All @@ -511,11 +513,19 @@ impl AuctionProvider for YourAuctionProvider {
&self,
request: &AuctionRequest,
_context: &AuctionContext<'_>,
) -> Result<AuctionResponse, Report<TrustedServerError>> {
) -> Result<PlatformPendingRequest, Report<TrustedServerError>> {
// 1. Transform AuctionRequest to your provider's format
// 2. Make HTTP request to your provider
// 3. Parse response
// 4. Return AuctionResponse with bids
// 2. Launch HTTP request through services.http_client().send_async(...)
// 3. Return PlatformPendingRequest for the orchestrator to await
todo!()
}

fn parse_response(
&self,
response: PlatformResponse,
response_time_ms: u64,
) -> Result<AuctionResponse, Report<TrustedServerError>> {
// 4. Parse PlatformResponse into AuctionResponse
todo!()
}

Expand Down Expand Up @@ -551,7 +561,7 @@ let orchestrator = AuctionOrchestrator::new(config);
orchestrator.register_provider(Arc::new(PrebidAuctionProvider::new(prebid_config)));
orchestrator.register_provider(Arc::new(ApsAuctionProvider::new(aps_config)));

let result = orchestrator.run_auction(&request, &context).await?;
let result = orchestrator.run_auction(&request, &context, &services).await?;

// Check results
assert_eq!(result.winning_bids.len(), 2);
Expand Down
7 changes: 2 additions & 5 deletions crates/trusted-server-core/src/auction/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use error_stack::{Report, ResultExt};
use http::{Request, Response};

use crate::auction::formats::AdRequest;
use crate::compat;
use crate::consent;
use crate::cookies::handle_request_cookies;
use crate::error::TrustedServerError;
Expand Down Expand Up @@ -87,13 +86,11 @@ pub async fn handle_auction(
geo,
)?;

let fastly_req = compat::to_fastly_request_ref(&http_req);

// Create auction context
let context = AuctionContext {
settings,
request: &fastly_req,
client_info: &services.client_info,
request: &http_req,
client_info: services.client_info(),
timeout_ms: settings.auction.timeout_ms,
provider_responses: None,
services,
Expand Down
66 changes: 23 additions & 43 deletions crates/trusted-server-core/src/auction/orchestrator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,32 +12,6 @@ use super::config::AuctionConfig;
use super::provider::AuctionProvider;
use super::types::{AuctionContext, AuctionRequest, AuctionResponse, Bid, BidStatus};

// # PR 15 removal target
//
// Mirrors compat::to_fastly_response — both should stay in sync until PR 15
// removes the compat layer entirely.
fn platform_response_to_fastly(
platform_resp: crate::platform::PlatformResponse,
) -> fastly::Response {
let (parts, body) = platform_resp.response.into_parts();
debug_assert!(
matches!(&body, edgezero_core::body::Body::Once(_)),
"unexpected Body::Stream in platform response conversion: body will be empty"
);
let body_bytes = match body {
edgezero_core::body::Body::Once(bytes) => bytes.to_vec(),
edgezero_core::body::Body::Stream(_) => {
log::warn!("streaming platform response body; body will be empty");
vec![]
}
};
let mut resp = fastly::Response::from_status(parts.status.as_u16());
for (name, value) in parts.headers.iter() {
resp.append_header(name.as_str(), value.as_bytes());
}
resp.set_body(body_bytes);
resp
}

/// Compute the remaining time budget from a deadline.
///
Expand Down Expand Up @@ -180,13 +154,14 @@ impl AuctionOrchestrator {
let start_time = Instant::now();
let pending = mediator
.request_bids(request, &mediator_context)
.await
.change_context(TrustedServerError::Auction {
message: format!("Mediator {} failed to launch", mediator.provider_name()),
})?;

let select_result = services
.http_client()
.select(vec![PlatformPendingRequest::new(pending)])
.select(vec![pending])
.await
.change_context(TrustedServerError::Auction {
message: format!("Mediator {} request failed", mediator.provider_name()),
Expand All @@ -197,11 +172,10 @@ impl AuctionOrchestrator {
.change_context(TrustedServerError::Auction {
message: format!("Mediator {} request failed", mediator.provider_name()),
})?;
let backend_response = platform_response_to_fastly(platform_resp);

let response_time_ms = start_time.elapsed().as_millis() as u64;
let mediator_resp = mediator
.parse_response(backend_response, response_time_ms)
.parse_response(platform_resp, response_time_ms)
.change_context(TrustedServerError::Auction {
message: format!("Mediator {} parse failed", mediator.provider_name()),
})?;
Expand Down Expand Up @@ -269,7 +243,7 @@ impl AuctionOrchestrator {

/// Run all providers in parallel and collect responses.
///
/// Uses `fastly::http::request::select()` to process responses as they
/// Uses `services.http_client().select(...)` to process responses as they
/// become ready, rather than waiting for each response sequentially.
async fn run_providers_parallel(
&self,
Expand Down Expand Up @@ -363,14 +337,17 @@ impl AuctionOrchestrator {
);

let start_time = Instant::now();
match provider.request_bids(request, &provider_context) {
match provider.request_bids(request, &provider_context).await {
Ok(pending) => {
let request_backend_name = pending
.backend_name()
.map(str::to_string)
.unwrap_or_else(|| backend_name.clone());
backend_to_provider.insert(
backend_name.clone(),
request_backend_name.clone(),
(provider.provider_name(), start_time, provider.as_ref()),
);
pending_requests
.push(PlatformPendingRequest::new(pending).with_backend_name(backend_name));
pending_requests.push(pending);
log::debug!(
"Request to '{}' launched successfully",
provider.provider_name()
Expand Down Expand Up @@ -418,14 +395,13 @@ impl AuctionOrchestrator {
Ok(platform_response) => {
// Identify the provider from the backend name
let backend_name = platform_response.backend_name.clone().unwrap_or_default();
let response = platform_response_to_fastly(platform_response);

if let Some((provider_name, start_time, provider)) =
backend_to_provider.remove(&backend_name)
{
let response_time_ms = start_time.elapsed().as_millis() as u64;

match provider.parse_response(response, response_time_ms) {
match provider.parse_response(platform_response, response_time_ms) {
Ok(auction_response) => {
log::info!(
"Provider '{}' returned {} bids (status: {:?}, time: {}ms)",
Expand Down Expand Up @@ -648,7 +624,6 @@ mod tests {
};
use crate::platform::test_support::noop_services;
use crate::test_support::tests::crate_test_settings_str;
use fastly::Request;
use std::collections::{HashMap, HashSet};

use super::AuctionOrchestrator;
Expand Down Expand Up @@ -702,7 +677,7 @@ mod tests {

fn create_test_context<'a>(
settings: &'a crate::settings::Settings,
req: &'a Request,
req: &'a http::Request<edgezero_core::body::Body>,
client_info: &'a crate::platform::ClientInfo,
) -> AuctionContext<'a> {
let services: &'static crate::platform::RuntimeServices =
Expand Down Expand Up @@ -774,18 +749,19 @@ mod tests {
}

// TODO: Re-enable provider integration tests after implementing mock support
// for send_async(). Mock providers can't create PendingRequest without real
// Fastly backends.
// for `PlatformHttpClient::send_async()`. Mock providers currently cannot
// create realistic pending requests for the select loop without real
// platform-backed transport handles.
//
// Untested timeout enforcement paths (require real backends):
// - Deadline check in select() loop (drops remaining requests)
// - Mediator skip when remaining_ms == 0 (bidding exhausts budget)
// - Provider skip when effective_timeout == 0 (budget exhausted before launch)
// - Provider context receives reduced timeout_ms per remaining budget
//
// Follow-up: introduce a thin abstraction over `select()` (e.g. a trait)
// Follow-up: introduce a thin abstraction over `PlatformHttpClient::select()`
// so the deadline/drop logic can be unit-tested with mock futures instead
// of requiring real Fastly backends. An `#[ignore]` integration test
// of requiring real platform backends. An `#[ignore]` integration test
// exercising the full path via Viceroy would also catch regressions.

#[tokio::test]
Expand All @@ -803,7 +779,11 @@ mod tests {

let request = create_test_auction_request();
let settings = create_test_settings();
let req = Request::get("https://test.com/test");
let req = http::Request::builder()
.method(http::Method::GET)
.uri("https://test.com/test")
.body(edgezero_core::body::Body::empty())
.expect("should build request");
let context = create_test_context(
&settings,
&req,
Expand Down
21 changes: 12 additions & 9 deletions crates/trusted-server-core/src/auction/provider.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
//! Trait definition for auction providers.

use async_trait::async_trait;
use error_stack::Report;
use fastly::http::request::PendingRequest;

use crate::error::TrustedServerError;
use crate::platform::{PlatformPendingRequest, PlatformResponse};

use super::types::{AuctionContext, AuctionRequest, AuctionResponse};

/// Trait implemented by all auction providers (Prebid, APS, GAM, etc.).
#[async_trait(?Send)]
pub trait AuctionProvider: Send + Sync {
/// Unique identifier for this provider (e.g., "prebid", "aps", "gam").
fn provider_name(&self) -> &'static str;
Expand All @@ -16,31 +18,32 @@ pub trait AuctionProvider: Send + Sync {
///
/// Implementations should:
/// - Transform `AuctionRequest` to provider-specific format
/// - Make HTTP call to provider endpoint using `send_async()`
/// - Return `PendingRequest` for orchestrator to await
/// - Make an HTTP call through `context.services.http_client().send_async(...)`
/// - Return [`PlatformPendingRequest`] for the orchestrator to await
///
/// The orchestrator will handle waiting for responses and parsing them.
///
/// # Errors
///
/// Returns an error if the request cannot be created or if the provider endpoint
/// cannot be reached (though usually network errors happen during `PendingRequest` await).
fn request_bids(
/// cannot be reached (though usually network errors happen while the returned
/// [`PlatformPendingRequest`] is polled).
async fn request_bids(
&self,
request: &AuctionRequest,
context: &AuctionContext<'_>,
) -> Result<PendingRequest, Report<TrustedServerError>>;
) -> Result<PlatformPendingRequest, Report<TrustedServerError>>;

/// Parse the response from the provider into an `AuctionResponse`.
///
/// Called by the orchestrator after the `PendingRequest` completes.
/// Called by the orchestrator after the [`PlatformPendingRequest`] completes.
///
/// # Errors
///
/// Returns an error if the response cannot be parsed into a valid `AuctionResponse`.
fn parse_response(
&self,
response: fastly::Response,
response: PlatformResponse,
response_time_ms: u64,
) -> Result<AuctionResponse, Report<TrustedServerError>>;

Expand All @@ -62,7 +65,7 @@ pub trait AuctionProvider: Send + Sync {
///
/// `timeout_ms` is the effective timeout that will be used when the backend
/// is registered in [`request_bids`](Self::request_bids). It must be
/// forwarded to [`BackendConfig::backend_name_for_url()`] so the predicted
/// forwarded to [`crate::backend::BackendConfig::backend_name_for_url`] so the predicted
/// name matches the actual registration (the timeout is part of the name).
fn backend_name(&self, _timeout_ms: u32) -> Option<String> {
None
Expand Down
5 changes: 3 additions & 2 deletions crates/trusted-server-core/src/auction/types.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Core types for auction requests and responses.

use fastly::Request;
use edgezero_core::body::Body as EdgeBody;
use http::Request;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

Expand Down Expand Up @@ -102,7 +103,7 @@ pub struct SiteInfo {
/// Context passed to auction providers.
pub struct AuctionContext<'a> {
pub settings: &'a Settings,
pub request: &'a Request,
pub request: &'a Request<EdgeBody>,
pub client_info: &'a ClientInfo,
pub timeout_ms: u32,
/// Provider responses from the bidding phase, used by mediators.
Expand Down
Loading
Loading