@@ -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) ]
370370mod 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