Skip to content

Log original request ID when reusing shared in-flight quote requests#4300

Merged
squadgazzz merged 2 commits intomainfrom
feat/request-sharing-observability
Mar 31, 2026
Merged

Log original request ID when reusing shared in-flight quote requests#4300
squadgazzz merged 2 commits intomainfrom
feat/request-sharing-observability

Conversation

@squadgazzz
Copy link
Copy Markdown
Contributor

@squadgazzz squadgazzz commented Mar 30, 2026

Background

Fast and optimal quoters share the same ExternalTradeFinder instance (via Arc<dyn TradeFinding>), so RequestSharing already deduplicates identical concurrent requests. However, when a hitchhiker reused an in-flight request, there was no trace linking it back to the original HTTP call, making it hard to debug why the optimal quoter failed to provide solutions, while the fast one succeeded for basically the same quote competition or vice versa.

Changes

  • Extended RequestSharing::shared_or_else to return a SharedResult that indicates whether an existing in-flight request was reused (is_shared flag), improving observability of request deduplication.
  • In ExternalTradeFinder, the HTTP request ID (X-REQUEST-ID) is now embedded in the shared future's result. When a caller (e.g. the fast or optimal quoter) reuses an in-flight request instead of sending a new one, a debug log is emitted with the original_request_id — making it possible to trace which HTTP request produced a given quote result.
  • All other shared_or_else callers updated to use the new .future field (no behavioral change).

How to test

Using staging, use the cowswap UI to get some quotes. Search for the new log.

@squadgazzz squadgazzz marked this pull request as ready for review March 30, 2026 17:58
@squadgazzz squadgazzz requested a review from a team as a code owner March 30, 2026 17:58
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request modifies the request-sharing library to return a SharedResult structure from the shared_or_else method, which includes a boolean flag indicating if a request was reused. This change is implemented across the driver, liquidity-sources, and price-estimation crates, allowing for improved logging and tracking of shared in-flight requests. I have no feedback to provide.

@squadgazzz squadgazzz force-pushed the feat/request-sharing-observability branch from 29f4585 to ef0d432 Compare March 30, 2026 18:03
@squadgazzz squadgazzz added the hotfix Labels PRs that should be applied into production right away label Mar 30, 2026
Copy link
Copy Markdown
Contributor

@jmg-duarte jmg-duarte left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM but I have notes, ultimately my concern is based on "if someone changes the code structure ever so slightly we might lose this log" — I'm aware my alternative isn't great either but should be slightly harder to misuse

I've numbered some comments so you're able to read them in order if GH messes up

Comment on lines +45 to +46
/// The (possibly shared) future to await.
pub future: Shared<Fut>,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 - IMO this could be a private field, and we'd implement Future for SharedResult instead

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — made future private and implemented Future for SharedResult, so callers can .await it directly.

Comment on lines +136 to +139
tracing::debug!(
original_request_id = ?response.request_id,
"reusing in-flight quote request"
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 - we'd log this inside the Future's await and have the request_id be conditionally attached via span

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The request_id lives inside SharedTradeResponse — it's only available after the future resolves, so it can't be placed in a span before the .await. The .instrument(info_span!("request sharing", original_request_id = ?response.request_id)) pattern from comment 3 would reference response before it exists.

Keeping the post-await check seems like the right fit here since the span data depends on the result. Open to other ideas if you see a way around this.

Comment on lines +132 to +140
let shared = self.sharing.shared_or_else(query.clone(), fut);
let response = shared.future.await;

if shared.is_shared {
tracing::debug!(
original_request_id = ?response.request_id,
"reusing in-flight quote request"
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 - which would turn this code into:

let shared_fut = self.sharing.shared_or_else(query.clone(), fut);
let response = if shared.is_shared {
    shared.instrument(info_span!("request sharing", original_request_id = ?response.request_id)).await
} else {
	shared.await
};

.send()
.await
.map_err(|err| PriceEstimationError::EstimatorInternal(anyhow!(err)))?;
if response.status() == 429 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Longer but no one needs to check what status code this is

Suggested change
if response.status() == 429 {
if response.status() == StatusCode::RateLimited {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — using StatusCode::TOO_MANY_REQUESTS.

Comment on lines +115 to +119
if let Ok(err) = serde_json::from_str::<dto::Error>(&text) {
PriceEstimationError::from(err)
} else {
PriceEstimationError::EstimatorInternal(anyhow!(err))
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if let Ok(err) = serde_json::from_str::<dto::Error>(&text) {
PriceEstimationError::from(err)
} else {
PriceEstimationError::EstimatorInternal(anyhow!(err))
}
serde_json::from_str::<dto::Error>(&text)
.map(PriceEstimationError::from)
.map_err(|err| PriceEstimationError::EstimatorInternal(anyhow!(err)))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The suggestion as-is returns Result<PriceEstimationError, PriceEstimationError> which doesn't collapse into the map_err closure's return type. Used unwrap_or_else to get the same effect:

serde_json::from_str::<dto::Error>(&text)
    .map(PriceEstimationError::from)
    .unwrap_or_else(|_| PriceEstimationError::EstimatorInternal(anyhow!(err)))

- Implement Future for SharedResult, making the inner future private
  so callers use .await directly instead of accessing .future
- Use StatusCode::TOO_MANY_REQUESTS instead of raw 429
- Refactor error parsing to use functional combinators
@squadgazzz squadgazzz added this pull request to the merge queue Mar 31, 2026
Merged via the queue into main with commit d067d68 Mar 31, 2026
19 checks passed
@squadgazzz squadgazzz deleted the feat/request-sharing-observability branch March 31, 2026 10:45
@github-actions github-actions bot locked and limited conversation to collaborators Mar 31, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

hotfix Labels PRs that should be applied into production right away

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants