Fetch uncached V3 pools on demand so cold quotes find liquidity#4560
Fetch uncached V3 pools on demand so cold quotes find liquidity#4560AryanGodara wants to merge 2 commits into
Conversation
c628569 to
a1aa54b
Compare
ec454b8 to
1d243f4
Compare
There was a problem hiding this comment.
Code Review
This pull request implements on-demand fetching for Uniswap V3 pools that are missing from the warm cache, allowing them to be fetched at the indexer's current block instead of waiting for the next maintenance cycle. However, two high-severity issues were identified: first, if no pools exist for the requested token pairs, the checkpoint block number defaults to 0, which triggers a massive event replay from block 1 and can cause database overload or OOM; second, passing 0 as the target block to fetch current pools breaks the subgraph client backend by querying the genesis block. Both issues require immediate fixes to ensure stability and compatibility.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
1d243f4 to
5bcf94d
Compare
a1aa54b to
30abab7
Compare
5bcf94d to
5df6d08
Compare
30abab7 to
eb207c2
Compare
5df6d08 to
706dac6
Compare
eb207c2 to
ea474c9
Compare
706dac6 to
8bfe6c4
Compare
ea474c9 to
8b03564
Compare
8bfe6c4 to
cf905d5
Compare
|
This pull request has been marked as stale because it has been inactive a while. Please update this pull request or it will be automatically closed. |
8b910f1 to
c860437
Compare
| // `0` means "latest available" (see the V3PoolDataSource contract). The | ||
| // subgraph needs a concrete block — `block: { number: 0 }` queries genesis | ||
| // — so resolve a recent safe block first. | ||
| let target_block = match target_block { | ||
| 0 => self.get_safe_block().await?, | ||
| n => n, | ||
| }; |
There was a problem hiding this comment.
nit:
| // `0` means "latest available" (see the V3PoolDataSource contract). The | |
| // subgraph needs a concrete block — `block: { number: 0 }` queries genesis | |
| // — so resolve a recent safe block first. | |
| let target_block = match target_block { | |
| 0 => self.get_safe_block().await?, | |
| n => n, | |
| }; | |
| // `0` means "latest available" for V3PoolDataSource | |
| let target_block = match target_block { | |
| // inversely, here `0` would be "genesis", so fetch the latest safe block | |
| 0 => self.get_safe_block().await?, | |
| n => n, | |
| }; |
| fn get( | ||
| &self, | ||
| token_pairs: &HashSet<TokenPair>, | ||
| ) -> (HashMap<Address, Arc<PoolInfo>>, Vec<Address>, u64) { |
There was a problem hiding this comment.
at what point a triple should become a struct so we get proper naming?
| (existing_pools, pools_checkpoint.block_number) | ||
| pools_checkpoint | ||
| .missing_pools | ||
| .extend(missing_pools.iter().copied()); |
There was a problem hiding this comment.
Vec::extend takes an IntoIterator which Vec should implement, so a single clone should work here
| /// skipping any that can't be converted yet (e.g. no ticks). Doesn't touch | ||
| /// the cache. Pass the checkpoint block to cache for replay, or 0 to take | ||
| /// the indexer's current snapshot without blocking. |
There was a problem hiding this comment.
Doesn't touch the cache. Pass the checkpoint block to cache for replay,
This is confusing, if this function doesnt touch the cache either dont mention it, or write a separate paragraph mentioning how it can be used/leveraged for caching purposes
| if pool_ids.is_empty() { | ||
| return Ok(Vec::new()); | ||
| } |
There was a problem hiding this comment.
IMO get_pools_with_ticks_by_ids should be written in such a way that this isn't needed
when you later collect its going to be an empty vec that ends equally empty
| let (missing, block_number) = { | ||
| let checkpoint = self.pools_checkpoint.lock().unwrap(); | ||
| ( | ||
| checkpoint.missing_pools.iter().copied().collect::<Vec<_>>(), |
There was a problem hiding this comment.
this looks like a clone to me
| // No pools registered for these pairs: `get` returns block 0, so skip the | ||
| // replay below (it would otherwise scan events from block 1) and return. |
There was a problem hiding this comment.
something like this, please double check I didn't butcher the doc meaning (I think I understood what it meant)
| // No pools registered for these pairs: `get` returns block 0, so skip the | |
| // replay below (it would otherwise scan events from block 1) and return. | |
| // if `get` returns block 0 that means there's no pools for these pairs | |
| // we skip the replay because it would scan events from block 1 onwards |
| } | ||
|
|
||
| // The warm cache only holds the top pools by raw liquidity, so plenty of | ||
| // real pairs are absent. Fetch those on demand instead of waiting for the |
There was a problem hiding this comment.
you're not fetching "on demand" here, just fetching them
| // Cold: not cached, flagged missing. | ||
| let (cached, missing, _) = handler.get(&HashSet::from([pair])); | ||
| assert!(cached.is_empty()); | ||
| assert_eq!(missing, vec![pool]); | ||
|
|
||
| // On-demand fetch resolves it. | ||
| let fetched = handler.fetch_current(&missing).await; | ||
| assert_eq!(fetched.len(), 1); | ||
| assert_eq!(fetched[0].0, pool); |
There was a problem hiding this comment.
shouldnt fetch_current work regardless if its in the cached or not?
if yes this test isn't really testing much other that you:
- can list non-cached pools
- fetch them
it seems like you're reconstructing logic that should be a single method 🤔
| /// minus the reorg buffer, the indexer serves finalized), so waiting | ||
| /// for it would hang the quote. Returns empty on failure so the quote | ||
| /// falls back to cached pools. | ||
| async fn fetch_current(&self, pool_ids: &[Address]) -> Vec<(Address, Arc<PoolInfo>)> { |
There was a problem hiding this comment.
| async fn fetch_current(&self, pool_ids: &[Address]) -> Vec<(Address, Arc<PoolInfo>)> { | |
| async fn fetch_at_current_block(&self, pool_ids: &[Address]) -> Vec<(Address, Arc<PoolInfo>)> { |
Description
The driver's Uniswap V3 fetcher warm-caches the top
max-pools-to-initializepools ranked by rawliquidity. Raw liquidity isn't comparable across pools (it scales with token decimals and price range), so well-funded pools with a low raw value are excluded from the warm cache. When a quote requests such a pair,checkpoint.get()returns nothing andfetchreturns zero AMMs, failing the quote until a later maintenance tick happens to back-fill the pool.This change makes
fetchfetch the requested pairs' uncached pools on demand instead of deferring to maintenance, so the first quote for any pair finds liquidity.Changes
PoolsCheckpointHandler::getalso returns the ids of pools that exist for the requested pairs but aren't cached.fetchfetches those on demand (newfetch_pools) before building the result; on failure it serves whatever is cached.fetch_poolsskips pools whose conversion fails (e.g. ticks not yet available) instead of aborting the batch;update_missing_poolsreuses it, so periodic maintenance gains the same resilience.Stacked on #4559 (bootstrap split) and #4555 (migration isolation) so one image carries all three for combined staging validation.
How to test
Unit + e2e tests.
Did a local driver against an ink-seeded pool-indexer. it loads the previously-missing pools on the first quote.