Skip to content

Commit 0d99fe6

Browse files
authored
Merge pull request #997 from tnull/2026-07-cancel-task-leaks
Release completed cancellable tasks
2 parents 93c06bc + 510e305 commit 0d99fe6

10 files changed

Lines changed: 595 additions & 145 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ base64 = { version = "0.22.1", default-features = false, features = ["std"] }
7070
getrandom = { version = "0.3", default-features = false }
7171
chrono = { version = "0.4", default-features = false, features = ["clock"] }
7272
tokio = { version = "1.39", default-features = false, features = [ "rt-multi-thread", "time", "sync", "macros", "net" ] }
73+
tokio-util = { version = "0.7.10", default-features = false, features = ["rt"] }
7374
esplora-client = { version = "0.12", default-features = false, features = ["tokio", "async-https-rustls"] }
7475
ldk-esplora-client = { package = "esplora-client", version = "0.13", default-features = false, features = ["tokio", "async-https-rustls"] }
7576
electrum-client = { version = "0.25", default-features = false, features = ["proxy", "use-rustls-ring"] }

src/chain/bitcoind.rs

Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ use lightning_block_sync::{
3131
};
3232
use serde::Serialize;
3333

34-
use super::WalletSyncStatus;
34+
use super::{WalletSyncGuard, WalletSyncStatus};
3535
use crate::config::{
3636
BitcoindRestClientConfig, Config, DEFAULT_FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS,
3737
DEFAULT_TX_BROADCAST_TIMEOUT_SECS,
@@ -52,6 +52,31 @@ const CHAIN_POLLING_TIMEOUT_SECS: u64 = 10;
5252
type BitcoindSpvClient =
5353
SpvClient<ChainPoller<Arc<BitcoindClient>, BitcoindClient>, Arc<ChainListener>>;
5454

55+
async fn acquire_initial_wallet_sync_guard<'a>(
56+
wallet_polling_status: &'a Mutex<WalletSyncStatus>,
57+
stop_sync_receiver: &mut tokio::sync::watch::Receiver<()>,
58+
) -> Option<WalletSyncGuard<'a>> {
59+
loop {
60+
let mut pending_sync = {
61+
let mut status_lock = wallet_polling_status.lock().expect("lock");
62+
match status_lock.register_or_subscribe_pending_sync() {
63+
Some(pending_sync) => pending_sync,
64+
None => {
65+
return Some(WalletSyncGuard::new(
66+
wallet_polling_status,
67+
Error::WalletOperationFailed,
68+
));
69+
},
70+
}
71+
};
72+
tokio::select! {
73+
biased;
74+
_ = stop_sync_receiver.changed() => return None,
75+
_ = pending_sync.recv() => {},
76+
}
77+
}
78+
}
79+
5580
pub(super) struct BitcoindChainSource {
5681
api_client: Arc<BitcoindClient>,
5782
spv_client: tokio::sync::Mutex<Option<BitcoindSpvClient>>,
@@ -160,12 +185,13 @@ impl BitcoindChainSource {
160185
) {
161186
// First register for the wallet polling status to make sure `Node::sync_wallets` calls
162187
// wait on the result before proceeding.
163-
{
164-
let mut status_lock = self.wallet_polling_status.lock().expect("lock");
165-
if status_lock.register_or_subscribe_pending_sync().is_some() {
166-
debug_assert!(false, "Sync already in progress. This should never happen.");
167-
}
168-
}
188+
let Some(initial_sync_guard) =
189+
acquire_initial_wallet_sync_guard(&self.wallet_polling_status, &mut stop_sync_receiver)
190+
.await
191+
else {
192+
log_trace!(self.logger, "Stopping initial chain sync.");
193+
return;
194+
};
169195

170196
log_info!(
171197
self.logger,
@@ -302,7 +328,7 @@ impl BitcoindChainSource {
302328
}
303329

304330
// Now propagate the initial result to unblock waiting subscribers.
305-
self.wallet_polling_status.lock().expect("lock").propagate_result_to_subscribers(Ok(()));
331+
initial_sync_guard.complete(Ok(()));
306332

307333
let mut chain_polling_interval =
308334
tokio::time::interval(Duration::from_secs(CHAIN_POLLING_INTERVAL_SECS));
@@ -413,6 +439,8 @@ impl BitcoindChainSource {
413439
Error::WalletOperationFailed
414440
})?;
415441
}
442+
let sync_guard =
443+
WalletSyncGuard::new(&self.wallet_polling_status, Error::WalletOperationFailed);
416444

417445
let res = self
418446
.poll_and_update_listeners_inner(
@@ -423,7 +451,7 @@ impl BitcoindChainSource {
423451
)
424452
.await;
425453

426-
self.wallet_polling_status.lock().expect("lock").propagate_result_to_subscribers(res);
454+
sync_guard.complete(res);
427455

428456
res
429457
}
@@ -1588,6 +1616,9 @@ impl std::error::Error for BitcoindClientError {}
15881616

15891617
#[cfg(test)]
15901618
mod tests {
1619+
use std::sync::Mutex;
1620+
use std::time::Duration;
1621+
15911622
use bitcoin::hashes::Hash;
15921623
use bitcoin::{FeeRate, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid, Witness};
15931624
use lightning_block_sync::http::JsonResponse;
@@ -1597,9 +1628,36 @@ mod tests {
15971628
use serde_json::json;
15981629

15991630
use crate::chain::bitcoind::{
1600-
FeeResponse, GetMempoolEntryResponse, GetRawMempoolResponse, GetRawTransactionResponse,
1601-
MempoolMinFeeResponse,
1631+
acquire_initial_wallet_sync_guard, FeeResponse, GetMempoolEntryResponse,
1632+
GetRawMempoolResponse, GetRawTransactionResponse, MempoolMinFeeResponse,
16021633
};
1634+
use crate::chain::{WalletSyncGuard, WalletSyncStatus};
1635+
use crate::Error;
1636+
1637+
#[tokio::test]
1638+
async fn initial_sync_waits_for_in_progress_sync() {
1639+
let status = Mutex::new(WalletSyncStatus::Completed);
1640+
assert!(status.lock().expect("lock").register_or_subscribe_pending_sync().is_none());
1641+
let in_progress_guard = WalletSyncGuard::new(&status, Error::WalletOperationFailed);
1642+
let (_stop_sender, mut stop_receiver) = tokio::sync::watch::channel(());
1643+
let mut acquire_guard =
1644+
Box::pin(acquire_initial_wallet_sync_guard(&status, &mut stop_receiver));
1645+
1646+
let early_result =
1647+
tokio::time::timeout(Duration::from_millis(10), acquire_guard.as_mut()).await;
1648+
assert!(early_result.is_err(), "background sync should wait for the active sync");
1649+
1650+
in_progress_guard.complete(Ok(()));
1651+
let acquired_guard = tokio::time::timeout(Duration::from_secs(1), acquire_guard)
1652+
.await
1653+
.expect("background sync should resume")
1654+
.expect("background sync should acquire the sync guard");
1655+
assert!(
1656+
matches!(*status.lock().expect("lock"), WalletSyncStatus::InProgress { .. }),
1657+
"background sync should own the next sync"
1658+
);
1659+
acquired_guard.complete(Ok(()));
1660+
}
16031661

16041662
prop_compose! {
16051663
fn arbitrary_witness()(

src/chain/electrum.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use lightning::chain::{Confirm, Filter, WatchedOutput};
2525
use lightning::util::ser::Writeable;
2626
use lightning_transaction_sync::ElectrumSyncClient;
2727

28-
use super::WalletSyncStatus;
28+
use super::{WalletSyncGuard, WalletSyncStatus};
2929
use crate::config::{
3030
clamp_full_scan_stop_gap, Config, ElectrumSyncConfig, MAX_FULL_SCAN_STOP_GAP,
3131
MIN_FULL_SCAN_STOP_GAP,
@@ -113,10 +113,12 @@ impl ElectrumChainSource {
113113
Error::WalletOperationFailed
114114
})?;
115115
}
116+
let sync_guard =
117+
WalletSyncGuard::new(&self.onchain_wallet_sync_status, Error::WalletOperationFailed);
116118

117119
let res = self.sync_onchain_wallet_inner(onchain_wallet).await;
118120

119-
self.onchain_wallet_sync_status.lock().expect("lock").propagate_result_to_subscribers(res);
121+
sync_guard.complete(res);
120122

121123
res
122124
}
@@ -223,14 +225,13 @@ impl ElectrumChainSource {
223225
Error::TxSyncFailed
224226
})?;
225227
}
228+
let sync_guard =
229+
WalletSyncGuard::new(&self.lightning_wallet_sync_status, Error::TxSyncFailed);
226230

227231
let res =
228232
self.sync_lightning_wallet_inner(channel_manager, chain_monitor, output_sweeper).await;
229233

230-
self.lightning_wallet_sync_status
231-
.lock()
232-
.expect("lock")
233-
.propagate_result_to_subscribers(res);
234+
sync_guard.complete(res);
234235

235236
res
236237
}

src/chain/esplora.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use lightning::chain::{Confirm, Filter, WatchedOutput};
1818
use lightning::util::ser::Writeable;
1919
use lightning_transaction_sync::EsploraSyncClient;
2020

21-
use super::WalletSyncStatus;
21+
use super::{WalletSyncGuard, WalletSyncStatus};
2222
use crate::config::{
2323
clamp_full_scan_stop_gap, Config, EsploraSyncConfig, BDK_CLIENT_CONCURRENCY,
2424
MAX_FULL_SCAN_STOP_GAP, MIN_FULL_SCAN_STOP_GAP,
@@ -133,10 +133,12 @@ impl EsploraChainSource {
133133
Error::WalletOperationFailed
134134
})?;
135135
}
136+
let sync_guard =
137+
WalletSyncGuard::new(&self.onchain_wallet_sync_status, Error::WalletOperationFailed);
136138

137139
let res = self.sync_onchain_wallet_inner(onchain_wallet).await;
138140

139-
self.onchain_wallet_sync_status.lock().expect("lock").propagate_result_to_subscribers(res);
141+
sync_guard.complete(res);
140142

141143
res
142144
}
@@ -283,14 +285,13 @@ impl EsploraChainSource {
283285
Error::WalletOperationFailed
284286
})?;
285287
}
288+
let sync_guard =
289+
WalletSyncGuard::new(&self.lightning_wallet_sync_status, Error::WalletOperationFailed);
286290

287291
let res =
288292
self.sync_lightning_wallet_inner(channel_manager, chain_monitor, output_sweeper).await;
289293

290-
self.lightning_wallet_sync_status
291-
.lock()
292-
.expect("lock")
293-
.propagate_result_to_subscribers(res);
294+
sync_guard.complete(res);
294295

295296
res
296297
}

src/chain/mod.rs

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,34 @@ pub(crate) enum WalletSyncStatus {
6565
InProgress { subscribers: tokio::sync::broadcast::Sender<Result<(), Error>> },
6666
}
6767

68+
pub(crate) struct WalletSyncGuard<'a> {
69+
status: &'a Mutex<WalletSyncStatus>,
70+
cancellation_error: Error,
71+
active: bool,
72+
}
73+
74+
impl<'a> WalletSyncGuard<'a> {
75+
pub(crate) fn new(status: &'a Mutex<WalletSyncStatus>, cancellation_error: Error) -> Self {
76+
Self { status, cancellation_error, active: true }
77+
}
78+
79+
pub(crate) fn complete(mut self, res: Result<(), Error>) {
80+
self.status.lock().expect("lock").propagate_result_to_subscribers(res);
81+
self.active = false;
82+
}
83+
}
84+
85+
impl Drop for WalletSyncGuard<'_> {
86+
fn drop(&mut self) {
87+
if self.active {
88+
self.status
89+
.lock()
90+
.expect("lock")
91+
.propagate_result_to_subscribers(Err(self.cancellation_error));
92+
}
93+
}
94+
}
95+
6896
impl WalletSyncStatus {
6997
fn register_or_subscribe_pending_sync(
7098
&mut self,
@@ -95,16 +123,7 @@ impl WalletSyncStatus {
95123
WalletSyncStatus::InProgress { subscribers } => {
96124
// A sync is in-progress, we notify subscribers.
97125
if subscribers.receiver_count() > 0 {
98-
match subscribers.send(res) {
99-
Ok(_) => (),
100-
Err(e) => {
101-
debug_assert!(
102-
false,
103-
"Failed to send wallet sync result to subscribers: {:?}",
104-
e
105-
);
106-
},
107-
}
126+
let _ = subscribers.send(res);
108127
}
109128
*self = WalletSyncStatus::Completed;
110129
},
@@ -561,3 +580,28 @@ impl Filter for ChainSource {
561580
}
562581
}
563582
}
583+
584+
#[cfg(test)]
585+
mod tests {
586+
use super::*;
587+
588+
#[test]
589+
fn wallet_sync_guard_resets_abandoned_sync() {
590+
let status = Mutex::new(WalletSyncStatus::Completed);
591+
assert!(status.lock().expect("lock").register_or_subscribe_pending_sync().is_none());
592+
let sync_guard = WalletSyncGuard::new(&status, Error::WalletOperationFailed);
593+
let mut subscriber = status
594+
.lock()
595+
.expect("lock")
596+
.register_or_subscribe_pending_sync()
597+
.expect("sync subscriber");
598+
599+
drop(sync_guard);
600+
601+
assert!(
602+
matches!(*status.lock().expect("lock"), WalletSyncStatus::Completed),
603+
"abandoned wallet sync should reset its status"
604+
);
605+
assert_eq!(subscriber.try_recv(), Ok(Err(Error::WalletOperationFailed)));
606+
}
607+
}

0 commit comments

Comments
 (0)