Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 12 additions & 9 deletions src/chain/bitcoind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1270,15 +1270,16 @@ impl BitcoindClient {

let mempool_entries_cache = mempool_entries_cache.lock().await;
let mut mempool_txs_cache = mempool_txs_cache.lock().await;
mempool_txs_cache.retain(|txid, _| mempool_entries_cache.contains_key(txid));

if let Some(difference) =
mempool_entries_cache.len().checked_sub(mempool_txs_cache.capacity())
{
mempool_txs_cache.reserve(difference)
}
// Only transactions we may have to emit again are cached, i.e., those that entered the
// mempool above our best processed height. Caching every mempool transaction would keep a
// full copy of the mempool in memory.
mempool_txs_cache.retain(|txid, _| {

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.

To go with the insert condition suggested above, the retain could keep an entry while it may still need emitting, i.e. the same condition as the skip check further down:

mempool_txs_cache.retain(|txid, _| {
	mempool_entries_cache.get(txid).is_some_and(|entry| {
		entry.height > best_processed_height || entry.time > prev_mempool_time
	})
});

prev_mempool_time is already loaded at the top of this function. After a successful poll latest_mempool_timestamp advances, so the next retain drops everything that was only kept for the retry, and the steady-state cache stays as small as with the current PR. I haven't compiled this against your branch, so please double-check it.

mempool_entries_cache
.get(txid)
.is_some_and(|entry| entry.height > best_processed_height)
});

let mut txs_to_emit = Vec::with_capacity(mempool_entries_cache.len());
let mut txs_to_emit = Vec::new();
for (txid, entry) in mempool_entries_cache.iter() {
if entry.time > latest_time {
latest_time = entry.time;
Expand All @@ -1302,7 +1303,9 @@ impl BitcoindClient {

match self.get_raw_transaction(&entry.txid).await {
Ok(Some(tx)) => {
mempool_txs_cache.insert(entry.txid, (tx.clone(), entry.time));
if !ancestor_within_height {

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.

One concern with caching only !ancestor_within_height txs: if a getrawtransaction fails halfway through the first big poll, we return Err below (before latest_mempool_timestamp is stored), so the retry has no cache to resume from and refetches every ancestor_within_height tx. At startup latest_mempool_timestamp is 0, so that is nearly the whole mempool. The old code resumed from the cache.

In a quick model (20k-entry mempool at startup, transient failure per RPC), a 1-in-10,000 failure rate went from ~2.9 polls / ~20k RPCs with the old code to ~6.7 polls / ~58k RPCs with this PR, and the cost grows quickly with mempool size and error rate.

What we need to keep is exactly the set that may still be emitted, so the insert could be:

if !ancestor_within_height || entry.time > prev_mempool_time {
	mempool_txs_cache.insert(entry.txid, (tx.clone(), entry.time));
}

Steady-state memory stays small, since a successful poll advances latest_mempool_timestamp, so the next retain drops these entries.

mempool_txs_cache.insert(entry.txid, (tx.clone(), entry.time));
}
txs_to_emit.push((tx, entry.time));
},
Ok(None) => {
Expand Down
Loading