Skip to content

Commit 5043bc9

Browse files
committed
graph, node, store: Add max_contracts parameter to clear_stale_call_cache
When --max-contracts is passed alongside --ttl-days, compute an effective TTL that limits eviction to at most that many contracts. The effective TTL is determined by looking at the (max_contracts+1)th oldest stale entry in call_meta; if it exists, the cutoff is set to its accessed_at date so that contracts at that date are excluded. When multiple contracts share the boundary date, fewer than max_contracts may be deleted since the cutoff operates at date granularity.
1 parent d42d1c7 commit 5043bc9

6 files changed

Lines changed: 103 additions & 10 deletions

File tree

graph/src/blockchain/mock.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,11 @@ impl ChainStore for MockChainStore {
592592
async fn clear_call_cache(&self, _from: BlockNumber, _to: BlockNumber) -> Result<(), Error> {
593593
unimplemented!()
594594
}
595-
async fn clear_stale_call_cache(&self, _ttl_days: usize) -> Result<(), Error> {
595+
async fn clear_stale_call_cache(
596+
&self,
597+
_ttl_days: usize,
598+
_max_contracts: Option<usize>,
599+
) -> Result<(), Error> {
596600
unimplemented!()
597601
}
598602
async fn chain_identifier(&self) -> Result<ChainIdentifier, Error> {

graph/src/components/store/traits.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -657,8 +657,14 @@ pub trait ChainStore: ChainHeadStore {
657657
/// Clears call cache of the chain for the given `from` and `to` block number.
658658
async fn clear_call_cache(&self, from: BlockNumber, to: BlockNumber) -> Result<(), Error>;
659659

660-
/// Clears stale call cache entries for the given TTL in days.
661-
async fn clear_stale_call_cache(&self, ttl_days: usize) -> Result<(), Error>;
660+
/// Clears stale call cache entries for the given TTL in days. If
661+
/// `max_contracts` is set, increase the effective TTL so that at
662+
/// most `max_contracts` contracts are evicted.
663+
async fn clear_stale_call_cache(
664+
&self,
665+
ttl_days: usize,
666+
max_contracts: Option<usize>,
667+
) -> Result<(), Error>;
662668

663669
/// Return the chain identifier for this store.
664670
async fn chain_identifier(&self) -> Result<ChainIdentifier, Error>;

node/src/bin/manager.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -620,7 +620,11 @@ pub enum CallCacheCommand {
620620
remove_entire_cache: bool,
621621
/// Remove the cache for contracts that have not been accessed in the last <TTL_DAYS> days
622622
#[clap(long, conflicts_with_all = &["from", "to", "remove-entire-cache"], value_parser = clap::value_parser!(u32).range(1..))]
623-
ttl_days: Option<usize>,
623+
ttl_days: Option<u32>,
624+
/// Maximum number of contracts to evict. When set, the effective TTL
625+
/// is increased so that at most this many contracts are deleted.
626+
#[clap(long, requires = "ttl_days")]
627+
max_contracts: Option<usize>,
624628
/// Starting block number
625629
#[clap(long, short, conflicts_with = "remove-entire-cache", requires = "to")]
626630
from: Option<i32>,
@@ -1546,12 +1550,14 @@ async fn main() -> anyhow::Result<()> {
15461550
to,
15471551
remove_entire_cache,
15481552
ttl_days,
1553+
max_contracts,
15491554
} => {
15501555
let chain_store = ctx.chain_store(&chain_name).await?;
15511556
if let Some(ttl_days) = ttl_days {
15521557
return commands::chain::clear_stale_call_cache(
15531558
chain_store,
1554-
ttl_days,
1559+
ttl_days as usize,
1560+
max_contracts,
15551561
)
15561562
.await;
15571563
}

node/src/manager/commands/chain.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,15 @@ pub async fn clear_call_cache(
8484
pub async fn clear_stale_call_cache(
8585
chain_store: Arc<ChainStore>,
8686
ttl_days: usize,
87+
max_contracts: Option<usize>,
8788
) -> Result<(), Error> {
8889
println!(
8990
"Removing stale entries from the call cache for `{}`",
9091
chain_store.chain
9192
);
92-
chain_store.clear_stale_call_cache(ttl_days).await?;
93+
chain_store
94+
.clear_stale_call_cache(ttl_days, max_contracts)
95+
.await?;
9396
Ok(())
9497
}
9598

store/postgres/src/chain_store.rs

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ mod data {
169169
pub(crate) const ETHEREUM_BLOCKS_TABLE_NAME: &str = "public.ethereum_blocks";
170170

171171
pub(crate) const ETHEREUM_CALL_CACHE_TABLE_NAME: &str = "public.eth_call_cache";
172+
pub(crate) const ETHEREUM_CALL_META_TABLE_NAME: &str = "public.eth_call_meta";
172173

173174
mod public {
174175
pub(super) use super::super::public::ethereum_networks;
@@ -1742,6 +1743,56 @@ mod data {
17421743
.map_err(StoreError::from)
17431744
}
17441745

1746+
/// Given `ttl_days` and `max_contracts`, compute an effective
1747+
/// TTL in days such that at most `max_contracts` contracts would
1748+
/// be evicted. Returns a value >= `ttl_days`.
1749+
///
1750+
/// We look at the (max_contracts+1)th oldest stale contract. If
1751+
/// it exists with `accessed_at = D`, the effective TTL is set to
1752+
/// `current_date - D` days, making the deletion cutoff exactly
1753+
/// `D`. Since the deletion query uses `accessed_at < cutoff`,
1754+
/// contracts with `accessed_at = D` are excluded, guaranteeing
1755+
/// at most `max_contracts` deletions.
1756+
///
1757+
/// When multiple contracts share the boundary date, we may
1758+
/// delete fewer than `max_contracts` since we can only control
1759+
/// the cutoff at date granularity.
1760+
pub(super) async fn effective_ttl(
1761+
&self,
1762+
conn: &mut AsyncPgConnection,
1763+
ttl_days: i64,
1764+
max_contracts: i64,
1765+
) -> Result<i64, StoreError> {
1766+
#[derive(QueryableByName)]
1767+
struct Row {
1768+
#[diesel(sql_type = diesel::sql_types::BigInt)]
1769+
effective_ttl: i64,
1770+
}
1771+
1772+
let meta_table = match self {
1773+
Storage::Shared => ETHEREUM_CALL_META_TABLE_NAME,
1774+
Storage::Private(Schema { call_meta, .. }) => call_meta.qname.as_str(),
1775+
};
1776+
let query = format!(
1777+
"select extract(day from current_date - accessed_at)::int8 + 1\
1778+
as effective_ttl \
1779+
from {meta_table} \
1780+
where accessed_at < current_date - $1 * interval '1 day' \
1781+
order by accessed_at asc \
1782+
offset $2 \
1783+
limit 1"
1784+
);
1785+
let effective_ttl: Option<i64> = sql_query(&query)
1786+
.bind::<BigInt, _>(ttl_days)
1787+
.bind::<BigInt, _>(max_contracts)
1788+
.get_result::<Row>(conn)
1789+
.await
1790+
.optional()?
1791+
.map(|r| r.effective_ttl);
1792+
1793+
Ok(effective_ttl.unwrap_or(ttl_days))
1794+
}
1795+
17451796
/// Delete all call_meta entries with accessed_at older than
17461797
/// `ttl_days`. Should be called after all corresponding
17471798
/// call_cache rows have been deleted.
@@ -3226,7 +3277,11 @@ impl ChainStoreTrait for ChainStore {
32263277
Ok(())
32273278
}
32283279

3229-
async fn clear_stale_call_cache(&self, ttl_days: usize) -> Result<(), Error> {
3280+
async fn clear_stale_call_cache(
3281+
&self,
3282+
ttl_days: usize,
3283+
max_contracts: Option<usize>,
3284+
) -> Result<(), Error> {
32303285
const LOG_INTERVAL: Duration = Duration::from_mins(5);
32313286

32323287
let conn = &mut self.pool.get_permitted().await?;
@@ -3238,6 +3293,25 @@ impl ChainStoreTrait for ChainStore {
32383293
.await?;
32393294

32403295
let ttl_days = ttl_days as i64;
3296+
let effective_ttl = match max_contracts {
3297+
Some(max) => {
3298+
self.storage
3299+
.effective_ttl(conn, ttl_days, max as i64)
3300+
.await?
3301+
}
3302+
None => ttl_days,
3303+
};
3304+
3305+
if effective_ttl != ttl_days {
3306+
info!(
3307+
logger,
3308+
"adjusted ttl from {} to {} days to stay within {} contracts",
3309+
ttl_days,
3310+
effective_ttl,
3311+
max_contracts.unwrap()
3312+
);
3313+
}
3314+
32413315
let mut total_deleted: usize = 0;
32423316
let mut batch_count: usize = 0;
32433317
let mut batch_size = AdaptiveBatchSize::with_size(100);
@@ -3248,7 +3322,7 @@ impl ChainStoreTrait for ChainStore {
32483322
let start = Instant::now();
32493323
let deleted = self
32503324
.storage
3251-
.delete_stale_calls_batch(conn, ttl_days, current_size)
3325+
.delete_stale_calls_batch(conn, effective_ttl, current_size)
32523326
.await?;
32533327

32543328
batch_size.adapt(start.elapsed());
@@ -3273,7 +3347,7 @@ impl ChainStoreTrait for ChainStore {
32733347
}
32743348
}
32753349

3276-
let contracts_deleted = self.storage.delete_stale_meta(conn, ttl_days).await?;
3350+
let contracts_deleted = self.storage.delete_stale_meta(conn, effective_ttl).await?;
32773351

32783352
info!(
32793353
logger,

store/test-store/tests/postgres/chain_head.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -636,7 +636,7 @@ fn test_clear_stale_call_cache() {
636636
.await
637637
.unwrap();
638638
}
639-
let result = chain_store.clear_stale_call_cache(7).await;
639+
let result = chain_store.clear_stale_call_cache(7, None).await;
640640
assert!(result.is_ok());
641641

642642
// Confirm the call cache entry was removed

0 commit comments

Comments
 (0)