Skip to content

Commit 4d184bd

Browse files
committed
io: Read only as many objects as a store needs
Seeding a store meant reading its entire namespace, which for a bounded cache means fetching a node's whole payment history at startup only to drop all but the newest entries. The previous commit sidestepped that by not seeding the payment store at all, leaving it cold and no longer catching unreadable payment data at build time. Give the reader a bound instead, and seed the payment store with the newest 50 payments. That matches the storage backends' page size, so warming the cache costs a single page listing and one batch of reads, and the first page of `Node::list_payments` is answered without going to the store. Take the keys from the paginated listing rather than `KVStore::list`, which is documented to return them in arbitrary order and would therefore make "the newest 50" meaningless. Objects now come back in the store's own creation order, newest first, where before they came back in whatever order the reads happened to finish. Note the cache treats the objects it is seeded with as increasingly recently used, so a newest-first read has to be reversed before seeding, or the newest entries would be the first ones evicted. Co-Authored-By: HAL 9000
1 parent 6bdd6e0 commit 4d184bd

4 files changed

Lines changed: 305 additions & 42 deletions

File tree

src/builder.rs

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ use crate::config::{
5151
BitcoindRestClientConfig, Config, ElectrumSyncConfig, EsploraSyncConfig, HRNResolverConfig,
5252
TorConfig, DEFAULT_ESPLORA_SERVER_URL, DEFAULT_LOG_FILENAME, DEFAULT_LOG_LEVEL,
5353
DEFAULT_MAX_PROBE_AMOUNT_MSAT, DEFAULT_MIN_PROBE_AMOUNT_MSAT, PAYMENT_CACHE_CAPACITY,
54+
PAYMENT_CACHE_WARMUP_COUNT,
5455
};
5556
use crate::connection::ConnectionManager;
5657
use crate::data_store::{KeepAllEntries, KeepLeastRecentlyUsed};
@@ -61,8 +62,8 @@ use crate::gossip::GossipSource;
6162
use crate::io::sqlite_store::SqliteStore;
6263
use crate::io::utils::{
6364
open_or_migrate_fs_store, read_all_objects, read_event_queue,
64-
read_external_pathfinding_scores_from_cache, read_network_graph, read_node_metrics,
65-
read_output_sweeper, read_peer_info, read_scorer,
65+
read_external_pathfinding_scores_from_cache, read_n_objects, read_network_graph,
66+
read_node_metrics, read_output_sweeper, read_peer_info, read_scorer,
6667
};
6768
use crate::io::vss_store::VssStoreBuilder;
6869
use crate::io::{
@@ -1456,9 +1457,16 @@ fn build_with_store_internal(
14561457

14571458
let kv_store_ref = Arc::clone(&kv_store);
14581459
let logger_ref = Arc::clone(&logger);
1459-
let (node_metris_res, pending_payment_store_res, address_pool_res) =
1460-
runtime.block_on(async move {
1460+
let (payment_store_res, node_metris_res, pending_payment_store_res, address_pool_res) = runtime
1461+
.block_on(async move {
14611462
tokio::join!(
1463+
read_n_objects(
1464+
&*kv_store_ref,
1465+
PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
1466+
PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
1467+
PAYMENT_CACHE_WARMUP_COUNT,
1468+
Arc::clone(&logger_ref),
1469+
),
14621470
read_node_metrics(&*kv_store_ref, Arc::clone(&logger_ref)),
14631471
read_all_objects(
14641472
&*kv_store_ref,
@@ -1483,17 +1491,23 @@ fn build_with_store_internal(
14831491
},
14841492
};
14851493

1486-
// The payment store caches a bounded number of payments and reads the rest back on demand, so
1487-
// we start it empty rather than paying to read a node's entire payment history at startup only
1488-
// to immediately drop all but the most recent entries.
1489-
let payment_store = Arc::new(PaymentStore::new(
1490-
Vec::new(),
1491-
KeepLeastRecentlyUsed::new(PAYMENT_CACHE_CAPACITY),
1492-
PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE.to_string(),
1493-
PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE.to_string(),
1494-
Arc::clone(&kv_store),
1495-
Arc::clone(&logger),
1496-
));
1494+
let payment_store = match payment_store_res {
1495+
Ok(payments) => Arc::new(PaymentStore::new(
1496+
// The read hands us the newest payments first, while the cache treats the objects it
1497+
// is seeded with as increasingly recently used. Reverse them, so that the newest
1498+
// payment is the last one to be evicted rather than the first.
1499+
payments.into_iter().rev().collect(),
1500+
KeepLeastRecentlyUsed::new(PAYMENT_CACHE_CAPACITY),
1501+
PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE.to_string(),
1502+
PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE.to_string(),
1503+
Arc::clone(&kv_store),
1504+
Arc::clone(&logger),
1505+
)),
1506+
Err(e) => {
1507+
log_error!(logger, "Failed to read payment data from store: {}", e);
1508+
return Err(BuildError::ReadFailed);
1509+
},
1510+
};
14971511

14981512
let (chain_source, chain_tip_opt) = match chain_data_source_config {
14991513
Some(ChainDataSourceConfig::Esplora { server_url, headers, sync_config }) => {

src/config.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ pub(crate) const DEFAULT_TX_BROADCAST_TIMEOUT_SECS: u64 = 10;
5757
// while still covering the recent payments a node actually works with.
5858
pub(crate) const PAYMENT_CACHE_CAPACITY: NonZeroUsize = NonZeroUsize::new(1000).unwrap();
5959

60+
// The number of payments we read into the cache when starting up.
61+
//
62+
// This matches the storage backends' page size, so warming the cache costs a single page listing
63+
// and one batch of reads, and the first page of `Node::list_payments` is served without going to
64+
// the store at all. The remaining capacity fills as payments are used.
65+
pub(crate) const PAYMENT_CACHE_WARMUP_COUNT: NonZeroUsize = NonZeroUsize::new(50).unwrap();
66+
6067
// The default {Esplora,Electrum} client timeout we're using.
6168
const DEFAULT_PER_REQUEST_TIMEOUT_SECS: u8 = 10;
6269

src/data_store.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,10 @@ where
298298
/// `objects` seeds the cache and must already be persisted under that namespace: under a
299299
/// bounded policy any object beyond `cache_policy`'s capacity is dropped from memory
300300
/// immediately, and is only recoverable by reading it back from the store.
301+
///
302+
/// They are taken in ascending order of recency, i.e., the last one given is treated as the
303+
/// most recently used and is therefore the last to be evicted. Callers seeding from a
304+
/// newest-first source have to reverse it.
301305
pub(crate) fn new(
302306
objects: Vec<SO>, cache_policy: P, primary_namespace: String, secondary_namespace: String,
303307
kv_store: Arc<DynStore>, logger: L,
@@ -1648,6 +1652,30 @@ mod tests {
16481652
.is_err());
16491653
}
16501654

1655+
#[tokio::test]
1656+
async fn lru_seeding_treats_the_last_object_as_most_recently_used() {
1657+
// The builder relies on this to hand a newest-first read over in reverse: whichever
1658+
// objects are given last must be the ones that survive, or seeding the cache would
1659+
// preferentially throw away the newest entries.
1660+
let kv_store = in_memory_store();
1661+
let seed_store = new_data_store(Arc::clone(&kv_store), KeepAllEntries, Vec::new());
1662+
let mut objects = Vec::new();
1663+
for i in 0..5u8 {
1664+
let object = TestObject::new(test_id(i), [i; 3]);
1665+
seed_store.insert(object).await.unwrap();
1666+
objects.push(object);
1667+
}
1668+
1669+
let data_store = new_data_store(kv_store, keep_lru(2), objects.clone());
1670+
1671+
assert_eq!(2, data_store.cached_len());
1672+
assert!(data_store.is_cached(&objects[3].id));
1673+
assert!(data_store.is_cached(&objects[4].id));
1674+
for object in objects.iter().take(3) {
1675+
assert!(!data_store.is_cached(&object.id));
1676+
}
1677+
}
1678+
16511679
#[tokio::test]
16521680
async fn lru_seeding_trims_to_capacity() {
16531681
let kv_store = in_memory_store();

0 commit comments

Comments
 (0)