Skip to content

Commit a201d40

Browse files
committed
Retry failed LSP protocol discovery
Add a background task that retries discovery for LSPs still undiscovered after the startup batch, with exponential backoff (5s up to a 1h cap), until every configured LSP is discovered. This recovers LSPs that were unreachable at startup instead of leaving them permanently unusable. Also coalesce concurrent discovery for the same LSP, so the retry task and a runtime add_liquidity_source don't race and needlessly fail.
1 parent dc4fa49 commit a201d40

3 files changed

Lines changed: 88 additions & 50 deletions

File tree

src/config.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,13 @@ 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 discovery-retry backoff ramps up to, and the interval it keeps retrying at
135+
// thereafter until every configured LSP has been discovered.
136+
pub(crate) const LIQUIDITY_DISCOVERY_RETRY_MAX_DELAY: Duration = Duration::from_secs(60 * 60);
137+
131138
#[derive(Debug, Clone)]
132139
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
133140
/// Represents the configuration of an [`Node`] instance.

src/lib.rs

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

192+
use crate::config::{LIQUIDITY_DISCOVERY_RETRY_INITIAL_DELAY, LIQUIDITY_DISCOVERY_RETRY_MAX_DELAY};
192193
use crate::ffi::maybe_wrap;
193194
use crate::liquidity::Liquidity;
194195
use crate::scoring::setup_background_pathfinding_scores_sync;
@@ -706,33 +707,7 @@ impl Node {
706707
let logger = Arc::clone(&discovery_logger);
707708
let ls = Arc::clone(&liquidity_handler);
708709
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-
}
710+
connect_and_discover_lsp(&cm, &ls, &logger, node_id, address).await;
736711
});
737712
}
738713

@@ -762,6 +737,39 @@ impl Node {
762737
}
763738
});
764739

740+
// Retry protocol discovery for any LSPs that failed the startup batch, backing off up to a
741+
// cap and then retrying at that cap until every configured LSP has been discovered.
742+
let mut stop_retry = self.stop_sender.subscribe();
743+
let retry_ls = Arc::clone(&self.liquidity_source);
744+
let retry_logger = Arc::clone(&self.logger);
745+
let retry_cm = Arc::clone(&self.connection_manager);
746+
self.runtime.spawn_cancellable_background_task(async move {
747+
let mut backoff = LIQUIDITY_DISCOVERY_RETRY_INITIAL_DELAY;
748+
loop {
749+
tokio::select! {
750+
_ = stop_retry.changed() => return,
751+
_ = tokio::time::sleep(backoff) => {},
752+
}
753+
754+
let undiscovered_lsps = retry_ls.get_undiscovered_lsps();
755+
if undiscovered_lsps.is_empty() {
756+
break;
757+
}
758+
759+
let mut discovery_set = tokio::task::JoinSet::new();
760+
for (node_id, address) in undiscovered_lsps {
761+
let cm = Arc::clone(&retry_cm);
762+
let ls = Arc::clone(&retry_ls);
763+
let logger = Arc::clone(&retry_logger);
764+
discovery_set.spawn(async move {
765+
connect_and_discover_lsp(&cm, &ls, &logger, node_id, address).await;
766+
});
767+
}
768+
discovery_set.join_all().await;
769+
backoff = (backoff * 2).min(LIQUIDITY_DISCOVERY_RETRY_MAX_DELAY);
770+
}
771+
});
772+
765773
log_info!(self.logger, "Startup complete.");
766774
*is_running_lock = true;
767775
Ok(())
@@ -2435,6 +2443,23 @@ pub(crate) fn new_channel_anchor_reserve_sats(
24352443
})
24362444
}
24372445

2446+
async fn connect_and_discover_lsp(
2447+
connection_manager: &ConnectionManager<Arc<Logger>>,
2448+
liquidity_source: &LiquiditySource<Arc<Logger>>, logger: &Logger, node_id: PublicKey,
2449+
address: SocketAddress,
2450+
) {
2451+
if let Err(e) = connection_manager.connect_peer_if_necessary(node_id, address).await {
2452+
log_debug!(logger, "Failed to connect to LSP {} for protocol discovery: {}", node_id, e);
2453+
return;
2454+
}
2455+
match liquidity_source.discover_lsp_protocols(&node_id).await {
2456+
Ok(protocols) => {
2457+
log_info!(logger, "Discovered protocols for LSP {}: {:?}", node_id, protocols)
2458+
},
2459+
Err(e) => log_debug!(logger, "Protocol discovery failed for LSP {}: {:?}", node_id, e),
2460+
}
2461+
}
2462+
24382463
#[cfg(test)]
24392464
mod tests {
24402465
use lightning::util::ser::{Readable, Writeable};

src/liquidity/mod.rs

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ where
345345
lsps1_client: Arc<LSPS1Client<L>>,
346346
lsps2_client: Arc<LSPS2Client<L>>,
347347
lsps2_service: Arc<LSPS2ServiceLiquiditySource<L>>,
348-
pending_lsps0_discovery: Mutex<HashMap<PublicKey, oneshot::Sender<Vec<u16>>>>,
348+
pending_lsps0_discovery: Mutex<HashMap<PublicKey, Vec<oneshot::Sender<Vec<u16>>>>>,
349349
discovery_done_tx: tokio::sync::watch::Sender<bool>,
350350
discovery_done_rx: tokio::sync::watch::Receiver<bool>,
351351
liquidity_manager: Arc<LiquidityManager>,
@@ -383,21 +383,14 @@ where
383383
protocols,
384384
}) => {
385385
if self.is_lsps_node(&counterparty_node_id) {
386-
if let Some(sender) = self
386+
if let Some(senders) = self
387387
.pending_lsps0_discovery
388388
.lock()
389389
.expect("lock")
390390
.remove(&counterparty_node_id)
391391
{
392-
match sender.send(protocols) {
393-
Ok(()) => (),
394-
Err(_) => {
395-
log_error!(
396-
self.logger,
397-
"Failed to handle response for request {:?} from liquidity service",
398-
counterparty_node_id
399-
);
400-
},
392+
for sender in senders {
393+
let _ = sender.send(protocols.clone());
401394
}
402395
} else {
403396
log_error!(
@@ -438,24 +431,23 @@ where
438431
let lsps0_handler = self.liquidity_manager.lsps0_client_handler();
439432

440433
let (sender, receiver) = oneshot::channel();
441-
{
434+
let issued_request = {
442435
let mut pending_discovery = self.pending_lsps0_discovery.lock().expect("lock");
443436
match pending_discovery.entry(*node_id) {
444-
Entry::Occupied(_) => {
445-
log_error!(
446-
self.logger,
447-
"LSPS0 protocol discovery already in flight for {}",
448-
node_id
449-
);
450-
return Err(Error::LiquidityRequestFailed);
437+
Entry::Occupied(mut e) => {
438+
e.get_mut().push(sender);
439+
false
451440
},
452441
Entry::Vacant(v) => {
453-
v.insert(sender);
442+
v.insert(vec![sender]);
454443
lsps0_handler.list_protocols(node_id);
444+
true
455445
},
456446
}
457-
}
447+
};
458448

449+
// Only the request that issued the discovery may remove the entry; a follower removing it
450+
// would drop the in-flight request and all other waiters.
459451
let protocols =
460452
tokio::time::timeout(Duration::from_secs(LIQUIDITY_REQUEST_TIMEOUT_SECS), receiver)
461453
.await
@@ -466,7 +458,9 @@ where
466458
node_id,
467459
e
468460
);
469-
self.pending_lsps0_discovery.lock().expect("lock").remove(node_id);
461+
if issued_request {
462+
self.pending_lsps0_discovery.lock().expect("lock").remove(node_id);
463+
}
470464
Error::LiquidityRequestFailed
471465
})?
472466
.map_err(|e| {
@@ -476,7 +470,9 @@ where
476470
node_id,
477471
e
478472
);
479-
self.pending_lsps0_discovery.lock().expect("lock").remove(node_id);
473+
if issued_request {
474+
self.pending_lsps0_discovery.lock().expect("lock").remove(node_id);
475+
}
480476
Error::LiquidityRequestFailed
481477
})?;
482478

@@ -517,6 +513,16 @@ where
517513
select_lsps_for_protocol(&self.lsp_nodes, protocol, Some(node_id))
518514
}
519515

516+
pub(crate) fn get_undiscovered_lsps(&self) -> Vec<(PublicKey, SocketAddress)> {
517+
self.lsp_nodes
518+
.read()
519+
.expect("lock")
520+
.iter()
521+
.filter(|n| n.supported_protocols.is_none())
522+
.map(|n| (n.node_id, n.address.clone()))
523+
.collect()
524+
}
525+
520526
/// Flips the `discovery_done` watch to `true`.
521527
///
522528
/// Called once after the *initial* batch of LSPs configured at build time has been

0 commit comments

Comments
 (0)