Skip to content

Commit 16f33f1

Browse files
committed
f Keep pending index consistent on prune fail
A failed batch prune can already have durably removed earlier entries. Keep the pending-payment secondary index in step with those partial removals so a retry can finish pruning and later reuse expired manual hashes. Co-Authored-By: HAL 9000
1 parent 1d29fcc commit 16f33f1

2 files changed

Lines changed: 140 additions & 5 deletions

File tree

src/data_store.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,14 @@ where
117117
}
118118

119119
pub(crate) async fn remove_batch(&self, ids: &[SO::Id]) -> Result<Vec<SO>, Error> {
120+
let (removed_objects, result) = self.remove_batch_with_partial_result(ids).await;
121+
result?;
122+
Ok(removed_objects)
123+
}
124+
125+
pub(crate) async fn remove_batch_with_partial_result(
126+
&self, ids: &[SO::Id],
127+
) -> (Vec<SO>, Result<(), Error>) {
120128
let _guard = self.mutation_lock.lock().await;
121129
let mut removed_objects = Vec::new();
122130
for id in ids {
@@ -126,7 +134,7 @@ where
126134
}
127135

128136
let store_key = id.encode_to_hex_str();
129-
KVStore::remove(
137+
let remove_result = KVStore::remove(
130138
&*self.kv_store,
131139
&self.primary_namespace,
132140
&self.secondary_namespace,
@@ -144,13 +152,16 @@ where
144152
e
145153
);
146154
Error::PersistenceFailed
147-
})?;
155+
});
156+
if let Err(e) = remove_result {
157+
return (removed_objects, Err(e));
158+
}
148159

149160
if let Some(object) = self.objects.lock().expect("lock").remove(id) {
150161
removed_objects.push(object);
151162
}
152163
}
153-
Ok(removed_objects)
164+
(removed_objects, Ok(()))
154165
}
155166

156167
/// Returns the current in-memory object for `id`.

src/payment/pending_payment_store.rs

Lines changed: 126 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,11 +272,11 @@ where
272272

273273
pub(crate) async fn remove_batch(&self, ids: &[PaymentId]) -> Result<(), Error> {
274274
let _guard = self.mutation_lock.lock().await;
275-
let removed_payments = self.inner.remove_batch(ids).await?;
275+
let (removed_payments, result) = self.inner.remove_batch_with_partial_result(ids).await;
276276
for payment in removed_payments {
277277
self.remove_from_index(&payment);
278278
}
279-
Ok(())
279+
result
280280
}
281281

282282
pub(crate) fn get(&self, id: &PaymentId) -> Option<PendingPaymentDetails> {
@@ -368,7 +368,13 @@ fn manual_bolt11_payment_hash(payment: &PaymentDetails) -> Option<PaymentHash> {
368368

369369
#[cfg(test)]
370370
mod tests {
371+
use std::future::Future;
372+
use std::pin::Pin;
373+
use std::sync::atomic::{AtomicUsize, Ordering};
374+
371375
use bitcoin::hashes::Hash;
376+
use lightning::io;
377+
use lightning::util::persist::{KVStore, PageToken, PaginatedKVStore, PaginatedListResponse};
372378
use lightning::util::test_utils::TestLogger;
373379
use lightning_types::payment::PaymentSecret;
374380

@@ -457,6 +463,83 @@ mod tests {
457463
)
458464
}
459465

466+
struct FailSecondRemoveStore {
467+
inner: InMemoryStore,
468+
remove_calls: AtomicUsize,
469+
}
470+
471+
impl FailSecondRemoveStore {
472+
fn new() -> Self {
473+
Self { inner: InMemoryStore::new(), remove_calls: AtomicUsize::new(0) }
474+
}
475+
}
476+
477+
impl KVStore for FailSecondRemoveStore {
478+
fn read(
479+
&self, primary_namespace: &str, secondary_namespace: &str, key: &str,
480+
) -> impl std::future::Future<Output = Result<Vec<u8>, io::Error>> + 'static + Send {
481+
KVStore::read(&self.inner, primary_namespace, secondary_namespace, key)
482+
}
483+
484+
fn write(
485+
&self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec<u8>,
486+
) -> impl std::future::Future<Output = Result<(), io::Error>> + 'static + Send {
487+
KVStore::write(&self.inner, primary_namespace, secondary_namespace, key, buf)
488+
}
489+
490+
fn remove(
491+
&self, primary_namespace: &str, secondary_namespace: &str, key: &str, _lazy: bool,
492+
) -> impl std::future::Future<Output = Result<(), io::Error>> + 'static + Send {
493+
let remove_call = self.remove_calls.fetch_add(1, Ordering::Relaxed) + 1;
494+
let fut: Pin<Box<dyn Future<Output = Result<(), io::Error>> + Send>> =
495+
if remove_call == 2 {
496+
Box::pin(async { Err(io::Error::new(io::ErrorKind::Other, "remove failed")) })
497+
} else {
498+
Box::pin(KVStore::remove(
499+
&self.inner,
500+
primary_namespace,
501+
secondary_namespace,
502+
key,
503+
false,
504+
))
505+
};
506+
fut
507+
}
508+
509+
fn list(
510+
&self, primary_namespace: &str, secondary_namespace: &str,
511+
) -> impl std::future::Future<Output = Result<Vec<String>, io::Error>> + 'static + Send {
512+
KVStore::list(&self.inner, primary_namespace, secondary_namespace)
513+
}
514+
}
515+
516+
impl PaginatedKVStore for FailSecondRemoveStore {
517+
fn list_paginated(
518+
&self, primary_namespace: &str, secondary_namespace: &str,
519+
page_token: Option<PageToken>,
520+
) -> impl std::future::Future<Output = Result<PaginatedListResponse, io::Error>> + 'static + Send
521+
{
522+
PaginatedKVStore::list_paginated(
523+
&self.inner,
524+
primary_namespace,
525+
secondary_namespace,
526+
page_token,
527+
)
528+
}
529+
}
530+
531+
fn pending_store_with_fail_second_remove() -> PendingPaymentStore<Arc<TestLogger>> {
532+
let store: Arc<DynStore> = Arc::new(DynStoreWrapper(FailSecondRemoveStore::new()));
533+
let logger = Arc::new(TestLogger::new());
534+
PendingPaymentStore::new(
535+
Vec::new(),
536+
"pending_payment_store_test_primary".to_string(),
537+
"pending_payment_store_test_secondary".to_string(),
538+
store,
539+
logger,
540+
)
541+
}
542+
460543
fn pending_manual_bolt11_payment(
461544
payment_hash: PaymentHash, payment_secret: Option<PaymentSecret>,
462545
) -> PendingPaymentDetails {
@@ -481,6 +564,17 @@ mod tests {
481564
)
482565
}
483566

567+
async fn prune_expired(
568+
store: &PendingPaymentStore<Arc<TestLogger>>, now: u64,
569+
) -> Result<(), Error> {
570+
let expired_payment_ids = store
571+
.list_filter(|payment| payment.has_expired(now, 0))
572+
.into_iter()
573+
.map(|payment| payment.details.id)
574+
.collect::<Vec<_>>();
575+
store.remove_batch(&expired_payment_ids).await
576+
}
577+
484578
#[tokio::test]
485579
async fn manual_bolt11_insert_rejects_duplicate_hash() {
486580
let store = pending_store();
@@ -500,6 +594,36 @@ mod tests {
500594
assert_eq!(store.get_pending_manual_bolt11_by_payment_hash(&payment_hash), Some(updated));
501595
}
502596

597+
#[tokio::test]
598+
async fn expired_manual_bolt11_entries_can_be_retried_after_partial_prune_failure() {
599+
let store = pending_store_with_fail_second_remove();
600+
let first_hash = PaymentHash([41; 32]);
601+
let second_hash = PaymentHash([42; 32]);
602+
assert_eq!(
603+
store.insert_manual_bolt11(pending_manual_bolt11_payment(first_hash, None)).await,
604+
Ok(())
605+
);
606+
assert_eq!(
607+
store.insert_manual_bolt11(pending_manual_bolt11_payment(second_hash, None)).await,
608+
Ok(())
609+
);
610+
611+
assert_eq!(prune_expired(&store, 1_000_001).await, Err(Error::PersistenceFailed));
612+
assert_eq!(store.list_filter(|_| true).len(), 1);
613+
614+
assert_eq!(prune_expired(&store, 1_000_001).await, Ok(()));
615+
assert!(store.list_filter(|_| true).is_empty());
616+
617+
assert_eq!(
618+
store.insert_manual_bolt11(pending_manual_bolt11_payment(first_hash, None)).await,
619+
Ok(())
620+
);
621+
assert_eq!(
622+
store.insert_manual_bolt11(pending_manual_bolt11_payment(second_hash, None)).await,
623+
Ok(())
624+
);
625+
}
626+
503627
#[test]
504628
fn pending_onchain_conflicts_exclude_current_txid_after_txid_rotation() {
505629
let original_txid = test_txid(1);

0 commit comments

Comments
 (0)