Skip to content

Introduce trait abstraction over select() to enable unit-testing timeout enforcement #473

Description

@ChristianPavilonis

Problem

The auction orchestrator's three timeout enforcement paths are untestable in unit tests:

  1. Deadline check in select() loop — drops remaining requests when auction_start.elapsed() >= deadline
  2. Mediator skip — skips mediator when remaining_ms == 0 after bidding phase
  3. Provider skip — skips provider launch when effective_timeout == 0

The root cause is tight coupling to Fastly's concrete PendingRequest and select():

  • PendingRequest::new() is pub(super) — cannot be constructed outside the fastly crate
  • select() is a free function backed by Wasm host ABI calls
  • The AuctionProvider trait's request_bids() returns PendingRequest directly

This means the orchestrator's select loop can only run on Fastly Compute (or Viceroy), never in a plain cargo test.

Suggested approach

Introduce a trait that abstracts over "a set of in-flight requests you can wait on":

/// Result from waiting on a set of pending provider requests.
pub struct ProviderResult {
    pub backend_name: String,
    pub result: Result<fastly::Response, fastly::http::request::SendError>,
}

/// Abstracts over the mechanism for waiting on concurrent provider requests.
///
/// Production uses Fastly's `select()`; tests inject a mock.
pub trait PendingRequestSet {
    /// True when there are no more requests to wait on.
    fn is_empty(&self) -> bool;

    /// Block until the next request completes and return it.
    fn select_next(&mut self) -> ProviderResult;
}

The orchestrator's select loop changes from:

while !remaining.is_empty() {
    let (result, rest) = select(remaining);
    remaining = rest;
    // ...
}

to:

while !pending.is_empty() {
    let result = pending.select_next();
    // ...
}

A production implementation wraps Vec<PendingRequest> and delegates to fastly::http::request::select(). A test implementation can return pre-canned responses with configurable delays (using std::thread::sleep or instant completion).

This unlocks tests like:

  • Provider skip: set timeout_ms: 0 on context, verify no requests are launched
  • Deadline enforcement: mock 3 slow providers, verify the loop breaks after the deadline
  • Mediator skip: inject provider responses that consume the full budget, verify mediator is skipped

Scope

  • No behavior change — pure structural refactor
  • Affected files: orchestrator.rs, provider.rs (trait may need adjustment), new test mock
  • The AuctionProvider::request_bids() return type may also need abstracting, or the trait boundary can be drawn at the select loop level only (simpler)

Context

Flagged during review of #469 (auction timeout enforcement). See the TODO at crates/common/src/auction/orchestrator.rs:711.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions