Skip to content

Commit 64b587f

Browse files
committed
Retry LSP protocol discovery and periodically re-discover
Add a background task that retries discovery for any LSP whose protocols are still undiscovered, using exponential backoff (5s up to 1h). Once no undiscovered LSPs remain (or the backoff is exhausted), the task settles into a fixed interval (24h) and re-runs discovery for all configured LSPs, so we also pick up protocols an LSP rolls out after we first connected.
1 parent dc4fa49 commit 64b587f

3 files changed

Lines changed: 121 additions & 29 deletions

File tree

src/config.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,17 @@ pub(crate) const HRN_RESOLUTION_TIMEOUT_SECS: u64 = 5;
128128
// The timeout after which we abort an LNURL-auth operation.
129129
pub(crate) const LNURL_AUTH_TIMEOUT_SECS: u64 = 15;
130130

131+
// The initial delay before retrying a failed liquidity protocol discovery operation.
132+
pub(crate) const LIQUIDITY_DISCOVERY_RETRY_INITIAL_DELAY: Duration = Duration::from_secs(5);
133+
134+
// The maximum delay the initial discovery-retry backoff ramps up to before handing off to the
135+
// periodic re-discovery sweep.
136+
pub(crate) const LIQUIDITY_DISCOVERY_RETRY_MAX_DELAY: Duration = Duration::from_secs(60 * 60);
137+
138+
// The interval at which we re-run protocol discovery for our configured LSPs, to pick up
139+
// protocols an LSP may have rolled out since the last discovery.
140+
pub(crate) const LIQUIDITY_REDISCOVERY_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60);
141+
131142
#[derive(Debug, Clone)]
132143
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
133144
/// Represents the configuration of an [`Node`] instance.

src/lib.rs

Lines changed: 81 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,10 @@ pub use types::{
189189
};
190190
pub use vss_client;
191191

192+
use crate::config::{
193+
LIQUIDITY_DISCOVERY_RETRY_INITIAL_DELAY, LIQUIDITY_DISCOVERY_RETRY_MAX_DELAY,
194+
LIQUIDITY_REDISCOVERY_INTERVAL,
195+
};
192196
use crate::ffi::maybe_wrap;
193197
use crate::liquidity::Liquidity;
194198
use crate::scoring::setup_background_pathfinding_scores_sync;
@@ -706,33 +710,7 @@ impl Node {
706710
let logger = Arc::clone(&discovery_logger);
707711
let ls = Arc::clone(&liquidity_handler);
708712
discovery_set.spawn(async move {
709-
if let Err(e) = cm.connect_peer_if_necessary(node_id, address.clone()).await {
710-
log_error!(
711-
logger,
712-
"Failed to connect to LSP {} for protocol discovery: {}",
713-
node_id,
714-
e
715-
);
716-
return;
717-
}
718-
match ls.discover_lsp_protocols(&node_id).await {
719-
Ok(protocols) => {
720-
log_info!(
721-
logger,
722-
"Discovered protocols for LSP {}: {:?}",
723-
node_id,
724-
protocols
725-
);
726-
},
727-
Err(e) => {
728-
log_error!(
729-
logger,
730-
"Failed to discover protocols for LSP {}: {:?}",
731-
node_id,
732-
e
733-
);
734-
},
735-
}
713+
connect_and_discover_lsp(&cm, &ls, &logger, node_id, address).await;
736714
});
737715
}
738716

@@ -762,6 +740,65 @@ impl Node {
762740
}
763741
});
764742

743+
// Retry protocol discovery for any LSPs that failed the startup batch, backing off
744+
// until we reach the periodic re-discovery cadence. From then on, re-discover all
745+
// configured LSPs on a fixed interval to pick up protocols they roll out later.
746+
let mut stop_rediscovery = self.stop_sender.subscribe();
747+
let rediscovery_ls = Arc::clone(&self.liquidity_source);
748+
let rediscovery_logger = Arc::clone(&self.logger);
749+
let rediscovery_cm = Arc::clone(&self.connection_manager);
750+
self.runtime.spawn_cancellable_background_task(async move {
751+
// Fast retries for LSPs that failed the startup discovery batch, backing off
752+
// until we reach the periodic re-discovery cadence.
753+
let mut backoff = LIQUIDITY_DISCOVERY_RETRY_INITIAL_DELAY;
754+
while backoff < LIQUIDITY_DISCOVERY_RETRY_MAX_DELAY {
755+
tokio::select! {
756+
_ = stop_rediscovery.changed() => return,
757+
_ = tokio::time::sleep(backoff) => {},
758+
}
759+
760+
let undiscovered_lsps = rediscovery_ls.get_undiscovered_lsps();
761+
if undiscovered_lsps.is_empty() {
762+
break;
763+
}
764+
765+
let mut discovery_set = tokio::task::JoinSet::new();
766+
for (node_id, address) in undiscovered_lsps {
767+
let cm = Arc::clone(&rediscovery_cm);
768+
let ls = Arc::clone(&rediscovery_ls);
769+
let logger = Arc::clone(&rediscovery_logger);
770+
discovery_set.spawn(async move {
771+
connect_and_discover_lsp(&cm, &ls, &logger, node_id, address).await;
772+
});
773+
}
774+
discovery_set.join_all().await;
775+
backoff *= 2;
776+
}
777+
778+
// periodically re-discover all configured LSPs to pick up newly
779+
// rolled-out protocols and recover any nodes that never completed discovery.
780+
let mut interval = tokio::time::interval(LIQUIDITY_REDISCOVERY_INTERVAL);
781+
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
782+
interval.tick().await;
783+
loop {
784+
tokio::select! {
785+
_ = stop_rediscovery.changed() => return,
786+
_ = interval.tick() => {},
787+
}
788+
789+
let mut discovery_set = tokio::task::JoinSet::new();
790+
for (node_id, address) in rediscovery_ls.get_all_lsp_details() {
791+
let cm = Arc::clone(&rediscovery_cm);
792+
let ls = Arc::clone(&rediscovery_ls);
793+
let logger = Arc::clone(&rediscovery_logger);
794+
discovery_set.spawn(async move {
795+
connect_and_discover_lsp(&cm, &ls, &logger, node_id, address).await;
796+
});
797+
}
798+
discovery_set.join_all().await;
799+
}
800+
});
801+
765802
log_info!(self.logger, "Startup complete.");
766803
*is_running_lock = true;
767804
Ok(())
@@ -2435,6 +2472,23 @@ pub(crate) fn new_channel_anchor_reserve_sats(
24352472
})
24362473
}
24372474

2475+
async fn connect_and_discover_lsp(
2476+
connection_manager: &ConnectionManager<Arc<Logger>>,
2477+
liquidity_source: &LiquiditySource<Arc<Logger>>, logger: &Logger, node_id: PublicKey,
2478+
address: SocketAddress,
2479+
) {
2480+
if let Err(e) = connection_manager.connect_peer_if_necessary(node_id, address).await {
2481+
log_debug!(logger, "Failed to connect to LSP {} for protocol discovery: {}", node_id, e);
2482+
return;
2483+
}
2484+
match liquidity_source.discover_lsp_protocols(&node_id).await {
2485+
Ok(protocols) => {
2486+
log_info!(logger, "Discovered protocols for LSP {}: {:?}", node_id, protocols)
2487+
},
2488+
Err(e) => log_debug!(logger, "Protocol discovery failed for LSP {}: {:?}", node_id, e),
2489+
}
2490+
}
2491+
24382492
#[cfg(test)]
24392493
mod tests {
24402494
use lightning::util::ser::{Readable, Writeable};

src/liquidity/mod.rs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,21 @@ where
352352
logger: L,
353353
}
354354

355+
// Removes the pending LSPS0 discovery entry for a node when dropped, ensuring cleanup happens
356+
// even if the discovery future is cancelled mid-await (e.g., when `Node::stop` aborts the
357+
// background task). Otherwise a stale entry would reject any subsequent discovery attempt for
358+
// this node as already being in flight.
359+
struct PendingDiscoveryGuard<'a> {
360+
pending: &'a Mutex<HashMap<PublicKey, oneshot::Sender<Vec<u16>>>>,
361+
node_id: PublicKey,
362+
}
363+
364+
impl Drop for PendingDiscoveryGuard<'_> {
365+
fn drop(&mut self) {
366+
self.pending.lock().expect("lock").remove(&self.node_id);
367+
}
368+
}
369+
355370
impl<L: Deref> LiquiditySource<L>
356371
where
357372
L::Target: LdkLogger,
@@ -456,6 +471,10 @@ where
456471
}
457472
}
458473

474+
// Clean up the pending entry on every exit path, including cancellation of this future.
475+
let _guard =
476+
PendingDiscoveryGuard { pending: &self.pending_lsps0_discovery, node_id: *node_id };
477+
459478
let protocols =
460479
tokio::time::timeout(Duration::from_secs(LIQUIDITY_REQUEST_TIMEOUT_SECS), receiver)
461480
.await
@@ -466,7 +485,6 @@ where
466485
node_id,
467486
e
468487
);
469-
self.pending_lsps0_discovery.lock().expect("lock").remove(node_id);
470488
Error::LiquidityRequestFailed
471489
})?
472490
.map_err(|e| {
@@ -476,7 +494,6 @@ where
476494
node_id,
477495
e
478496
);
479-
self.pending_lsps0_discovery.lock().expect("lock").remove(node_id);
480497
Error::LiquidityRequestFailed
481498
})?;
482499

@@ -517,6 +534,16 @@ where
517534
select_lsps_for_protocol(&self.lsp_nodes, protocol, Some(node_id))
518535
}
519536

537+
pub(crate) fn get_undiscovered_lsps(&self) -> Vec<(PublicKey, SocketAddress)> {
538+
self.lsp_nodes
539+
.read()
540+
.expect("lock")
541+
.iter()
542+
.filter(|n| n.supported_protocols.is_none())
543+
.map(|n| (n.node_id, n.address.clone()))
544+
.collect()
545+
}
546+
520547
/// Flips the `discovery_done` watch to `true`.
521548
///
522549
/// Called once after the *initial* batch of LSPs configured at build time has been

0 commit comments

Comments
 (0)