Skip to content

Commit

Permalink
Respect deadline in Baseline solver (#2040)
Browse files Browse the repository at this point in the history
# Description
Baseline solver iterates through orders from `auction` and executes some
logic for each order in a synchronous way. It checks the deadline before
each order. It could happen that the new order logic begins to execute
1ms before the deadline and lasts arbitrary amount of time (500ms?) and
significantly break the deadline.

This code adds a checker if the deadline is reached in the main thread,
and if it is, ends the background work prematurely and returns whatever
was executed up until that point.

## How to test
Example:
https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=0e23fa016153883faad8b0e7148b4dfa
  • Loading branch information
sunce86 authored Nov 6, 2023
1 parent 5849d33 commit c908312
Showing 1 changed file with 82 additions and 63 deletions.
145 changes: 82 additions & 63 deletions crates/solvers/src/domain/solver/baseline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,80 +69,99 @@ impl Baseline {
// not lock up the [`tokio`] runtime and cause it to slow down handling
// the real async things. For larger settlements, this can block in the
// 100s of ms.
let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
let deadline = auction.deadline.remaining().unwrap_or_default();

let inner = self.0.clone();
let span = tracing::Span::current();
tokio::task::spawn_blocking(move || {
let background_work = async move {
let _entered = span.enter();
inner.solve(auction)
})
.await
.expect("baseline solver unexpected panic")
inner.solve(auction, sender).await;
};

if tokio::time::timeout(deadline, tokio::spawn(background_work))
.await
.is_err()
{
tracing::debug!("reached timeout while solving orders");
}

let mut solutions = vec![];
while let Ok(solution) = receiver.try_recv() {
solutions.push(solution);
}
solutions
}
}

impl Inner {
fn solve(&self, auction: auction::Auction) -> Vec<solution::Solution> {
async fn solve(
&self,
auction: auction::Auction,
sender: tokio::sync::mpsc::UnboundedSender<solution::Solution>,
) {
let boundary_solver =
boundary::baseline::Solver::new(&self.weth, &self.base_tokens, &auction.liquidity);

auction
.orders
.iter()
.enumerate()
.take_while(|_| auction.deadline.remaining().is_some())
.filter_map(|(i, order)| {
let sell_token = auction.tokens.reference_price(&order.sell.token);
self.requests_for_order(UserOrder::new(order)?)
.find_map(|request| {
tracing::trace!(order =% order.uid, ?request, "finding route");

let route = boundary_solver.route(request, self.max_hops)?;
let interactions = route
.segments
.iter()
.map(|segment| {
solution::Interaction::Liquidity(solution::LiquidityInteraction {
liquidity: segment.liquidity.clone(),
input: segment.input,
output: segment.output,
// TODO does the baseline solver know about this
// optimization?
internalize: false,
})
})
.collect();

// The baseline solver generates a path with swapping
// for exact output token amounts. This leads to
// potential rounding errors for buy orders, where we
// can buy slightly more than intended. Fix this by
// capping the output amount to the order's buy amount
// for buy orders.
let mut output = route.output();
if let order::Side::Buy = order.side {
output.amount = cmp::min(output.amount, order.buy.amount);
}

let score = solution::Score::RiskAdjusted(solution::SuccessProbability(
self.risk
.success_probability(route.gas(), auction.gas_price, 1),
));

Some(
solution::Single {
order: order.clone(),
input: route.input(),
output,
interactions,
gas: route.gas(),
}
.into_solution(auction.gas_price, sell_token, score)?
.with_id(solution::Id(i as u64))
.with_buffers_internalizations(&auction.tokens),
)
for (i, order) in auction.orders.into_iter().enumerate() {
let sell_token = auction.tokens.reference_price(&order.sell.token);
let Some(user_order) = UserOrder::new(&order) else {
continue;
};
let solution = self.requests_for_order(user_order).find_map(|request| {
tracing::trace!(order =% order.uid, ?request, "finding route");

let route = boundary_solver.route(request, self.max_hops)?;
let interactions = route
.segments
.iter()
.map(|segment| {
solution::Interaction::Liquidity(solution::LiquidityInteraction {
liquidity: segment.liquidity.clone(),
input: segment.input,
output: segment.output,
// TODO does the baseline solver know about this optimization?
internalize: false,
})
})
})
.collect()
.collect();

// The baseline solver generates a path with swapping
// for exact output token amounts. This leads to
// potential rounding errors for buy orders, where we
// can buy slightly more than intended. Fix this by
// capping the output amount to the order's buy amount
// for buy orders.
let mut output = route.output();
if let order::Side::Buy = order.side {
output.amount = cmp::min(output.amount, order.buy.amount);
}

let score = solution::Score::RiskAdjusted(solution::SuccessProbability(
self.risk
.success_probability(route.gas(), auction.gas_price, 1),
));

Some(
solution::Single {
order: order.clone(),
input: route.input(),
output,
interactions,
gas: route.gas(),
}
.into_solution(auction.gas_price, sell_token, score)?
.with_id(solution::Id(i as u64))
.with_buffers_internalizations(&auction.tokens),
)
});
if let Some(solution) = solution {
if sender.send(solution).is_err() {
tracing::debug!("deadline hit, receiver dropped");
break;
}
}
}
}

fn requests_for_order(&self, order: UserOrder) -> impl Iterator<Item = Request> {
Expand Down

0 comments on commit c908312

Please sign in to comment.