From 3059b37ad6802545d86c793cd9feac4833f13e29 Mon Sep 17 00:00:00 2001 From: amackillop Date: Thu, 16 Apr 2026 23:47:54 +0100 Subject: [PATCH] Decouple LSPS4 webhooks from liquidity loop Send LSPS4 wake-up webhooks in the background instead of awaiting the HTTP response on the liquidity event loop. Direct webhook delivery was meant to reduce wake-up latency, but waiting for the response made unrelated liquidity work depend on the client's webhook handler. When that handler keeps the request open while the node wakes and starts processing payments, follow-up actions such as opening a channel can be delayed behind the webhook. Keep a long timeout on the background request as cleanup for genuinely stuck connections. The timeout is not part of LSPS4 control flow; it is there to bound resource usage while still treating the webhook as best-effort. --- src/liquidity.rs | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/liquidity.rs b/src/liquidity.rs index 986cca1bbc..dbf3d1a323 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -61,6 +61,10 @@ use crate::fee_estimator::{self, ConfirmationTarget, FeeEstimator, OnchainFeeEst use crate::{total_anchor_channels_reserve_sats, Config, Error}; const LIQUIDITY_REQUEST_TIMEOUT_SECS: u64 = 5; +// Client webhooks may intentionally keep the HTTP response open while the node wakes +// and processes incoming payments. This timeout is only to clean up genuinely stuck +// requests without blocking LSPS4 liquidity actions. +const DIRECT_WEBHOOK_TIMEOUT_SECS: u64 = 70; const LSPS2_GETINFO_REQUEST_EXPIRY: Duration = Duration::from_secs(60 * 60 * 24); pub(crate) const LSPS2_CHANNEL_CLTV_EXPIRY_DELTA: u32 = 72; @@ -1332,15 +1336,36 @@ where let mut json_body = HashMap::new(); json_body.insert("nodeId", counterparty_node_id.to_string()); json_body.insert("paymentHash", payment_hash.to_string()); + let counterparty_node_id = counterparty_node_id.to_string(); + let payment_hash = payment_hash.to_string(); + let client = client.clone(); + let url = url.clone(); log_info!( self.logger, "Sending webhook directly for payment_hash={} node={}", payment_hash, counterparty_node_id ); - if let Err(e) = client.post(url).json(&json_body).send().await { - log_error!(self.logger, "Direct webhook POST failed: {:?}", e); - } + let _ = tokio::spawn(async move { + match tokio::time::timeout( + Duration::from_secs(DIRECT_WEBHOOK_TIMEOUT_SECS), + client.post(&url).json(&json_body).send(), + ) + .await + { + Ok(Ok(_)) => {}, + Ok(Err(e)) => { + log::error!("Direct webhook POST failed: {:?}", e); + }, + Err(_) => { + log::error!( + "Direct webhook POST timed out for payment_hash={} node={}", + payment_hash, + counterparty_node_id + ); + }, + } + }); } else { // Fallback: route through event queue (slower path via S3 persistence). log_info!(