Skip to content

Check banned users on order arrival - #4696

Open
jmg-duarte wants to merge 2 commits into
mainfrom
jmgd/pre-warm-banned
Open

Check banned users on order arrival#4696
jmg-duarte wants to merge 2 commits into
mainfrom
jmgd/pre-warm-banned

Conversation

@jmg-duarte

@jmg-duarte jmg-duarte commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Description

We're doing all the user ban checks when performing auction cutting, however, we can check them eagerly when the autopilot gets notified of new orders; this works because we can extract the owner from the order UID. As such we can check on order arrival leading to the cache being pre-warmed when the auction cutting happens.

Changes

  • New pre-fetch task for banned users, as orders arrive through NOTIFY we send the owner over the pipe
    • Sends are batched to avoid spawning a slew of tasks
  • Order listener now gets access to the pipe to send new orders

How to test

Tested in staging to ensure nothing broke and in prod:

Screenshot 2026-08-03 at 11 26 55

The overall latency was reduced as one can see based on less bucket variance, there are still some spikes as receivers are still searched later on, since the order UID doesn't carry info on them.

Heatmap panel:

sum by (le) (increase(gp_v2_autopilot_auction_update_stage_time_bucket{
  network="mainnet", stage="banned_user_filtering"}[$__rate_interval]))

@jmg-duarte
jmg-duarte marked this pull request as ready for review August 3, 2026 10:34
@jmg-duarte
jmg-duarte requested a review from a team as a code owner August 3, 2026 10:35
@MartinquaXD

Copy link
Copy Markdown
Contributor

Did this also have a measurable latency reduction when building auctions? Fetching banned users happens concurrently with other things so I might have 0 net impact if the other things we do concurrently always take longer than the ban check.

Comment thread crates/autopilot/src/run_loop.rs Outdated

// Spawn background tasks to listen for events
persistence.spawn_order_listener(wake_notify.clone());
persistence.spawn_order_listener(wake_notify.clone(), banned_users);

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.

Feels like the concept of handling order as they get put into the orderbook will become more relevant and should probably get its own dedicated domain level component.
That could then be used for:

  • pre-fetch ban status
  • pre-fetch balances
  • initiate fast path handling for the fast path feature currently in the works

Comment on lines +179 to +181
// Best effort: the receiver (distinct from the
// owner in ~3% of orders) is not derivable from
// the payload and remains a cut-time lookup

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.

Seems like a classical AI comment where context from the implementation thread gets added to the code base as if it makes sense universally.
In this case the code is not even looking at the order owner so this comment makes 0 sense.

@jmg-duarte jmg-duarte Aug 3, 2026

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.

🤦 I remember vetting this comment, then refactored and it got LLM'd

let (sender, mut receiver) = mpsc::channel(QUEUE_SIZE);
tokio::spawn(async move {
while let Some(address) = receiver.recv().await {
let mut batch = HashSet::from([address]);

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.

batching requests feels like unnecessary complexity at the moment. Did you evaluate how frequently new orders get placed?

Additionally I think the number of NEW owners/receivers is a lot lower still. If you expose a sync API on the banned users cache that only responds with cached data you can probably discard the majority of updates outright so that batching is really not needed.

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.

I added the batching to avoid launching multiple tasks per order, like when we get spammed and get a bunch of orders followed by a bunch of cancellations

The alternative actually involved adding more code to handle a single order instead of multiple ones, so code-wise this is slightly simpler

I can put the filter up though, before it even gets sent to the batch

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.

I added the batching to avoid launching multiple tasks per order

You can still have the 1 dedicated background task without batching, though. AFAICS none of the downstream components actually have any significant batch optimizations (hermod just has a single address API, the RPC calls automatically get batched, and the caches are using moka which is optimized for concurrent accesses).
Are there any significant downsides of just channel.for_each(banned_users.banned(user)) or something simple like that?

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.

Ah I see what you're saying, yes, we can keep a single task reading off the queue, the other points about the RPCs are valid too

Comment on lines +121 to +134
let deadline = tokio::time::sleep(BATCH_DELAY);
tokio::pin!(deadline);
loop {
tokio::select! {
() = &mut deadline => break,
next = receiver.recv() => {
let Some(address) = next else { break };
batch.insert(address);
if batch.len() >= BATCH_SIZE {
break;
}
}
}
}

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.

This is effectively chunks_timeout.

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.

Nice, didn't know about this one

@jmg-duarte

Copy link
Copy Markdown
Contributor Author

Did this also have a measurable latency reduction when building auctions?

This is part of the "update solvable orders" phase, sadly, it didn't have a measurable impact

@MartinquaXD

Copy link
Copy Markdown
Contributor

This is part of the "update solvable orders" phase, sadly, it didn't have a measurable impact

Keep in mind that real world improvements should be the measurement of success - especially for PRs introducing non-trivial changes. In this case I believe there is an argument to be made to add this component now because we already have a few additions to that in mind but without those additions I don't think this PR should be merged.

@jmg-duarte
jmg-duarte force-pushed the jmgd/pre-warm-banned branch from 0741b43 to 8619593 Compare August 3, 2026 14:40
}
};
// Owners we already know about make up the bulk of the arrivals.
if users.cached(&owner).is_none() {

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.

At this point there is not much point anymore in doing the cached check, right?
The regular banned() function already first looks in the cache.

tokio::spawn(async move {
while let Some(arrival) = arrivals.next().await {
let owner = match arrival {
Ok(order) => order.owner(),

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.

This currently only looks at the order's owner but not the receiver. For the other 2 use cases (balance cache warming, order fast path) we both need to look up the order. So probably best to already fetch the order from the DB in the new order_notify.


// Spawn background tasks to listen for events
persistence.spawn_order_listener(wake_notify.clone(), banned_users);
persistence.spawn_order_listener(wake_notify.clone(), order_notifier);

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.

Feels like the actual DB notify listener should now be spawned inside the constructor of the new component, no?
What I'd imagine is sth like:

pub struct NewOrderListener;

impl NewOrderListener {
    pub fn new() -> Self {
        // spawn DB NOTIFY listener and wire it up
    }
}

Also I think all the things that get done when an order gets posted should live in this struct instead of spreading that over many different background tasks created in infra files (e.g. ban list warming, balance cache warming, fast path handling).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants