Skip to content

Commit 08d3b5d

Browse files
committed
Release completed cancellable tasks
Avoid retaining completed Tokio task allocations for the node lifetime while preserving shutdown cancellation and restart semantics. Co-Authored-By: HAL 9000
1 parent 08efb3a commit 08d3b5d

2 files changed

Lines changed: 92 additions & 12 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", default-features = false, features = ["rt"] }
7374
esplora-client = { version = "0.12", default-features = false, features = ["tokio", "async-https-rustls"] }
7475
electrum-client = { version = "0.25", default-features = false, features = ["proxy", "use-rustls-ring"] }
7576
libc = "0.2"

src/runtime.rs

Lines changed: 91 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ use std::time::Duration;
1313

1414
use lightning::util::native_async::FutureSpawner;
1515
use tokio::task::{JoinHandle, JoinSet};
16+
use tokio_util::sync::CancellationToken;
17+
use tokio_util::task::TaskTracker;
1618

1719
use crate::config::{
1820
BACKGROUND_TASK_SHUTDOWN_TIMEOUT_SECS, LDK_EVENT_HANDLER_SHUTDOWN_TIMEOUT_SECS,
@@ -28,13 +30,18 @@ pub(crate) struct Runtime {
2830
}
2931

3032
struct CancellableBackgroundTasks {
31-
tasks: JoinSet<()>,
33+
tasks: TaskTracker,
34+
cancellation_token: CancellationToken,
3235
accepting_tasks: bool,
3336
}
3437

3538
impl CancellableBackgroundTasks {
3639
fn new() -> Self {
37-
Self { tasks: JoinSet::new(), accepting_tasks: true }
40+
Self {
41+
tasks: TaskTracker::new(),
42+
cancellation_token: CancellationToken::new(),
43+
accepting_tasks: true,
44+
}
3845
}
3946
}
4047

@@ -109,8 +116,7 @@ impl Runtime {
109116
where
110117
F: Future<Output = ()> + Send + 'static,
111118
{
112-
let mut cancellable_background_tasks =
113-
self.cancellable_background_tasks.lock().expect("lock");
119+
let cancellable_background_tasks = self.cancellable_background_tasks.lock().expect("lock");
114120
if !cancellable_background_tasks.accepting_tasks {
115121
log_trace!(
116122
self.logger,
@@ -122,11 +128,32 @@ impl Runtime {
122128
// Since it seems to make a difference to `tokio` (see
123129
// https://docs.rs/tokio/latest/tokio/time/fn.timeout.html#panics) we make sure the futures
124130
// are always put in an `async` / `.await` closure.
125-
cancellable_background_tasks.tasks.spawn_on(async { future.await }, runtime_handle);
131+
let cancellation_token = cancellable_background_tasks.cancellation_token.clone();
132+
// Detach the handle while the tracker continues tracking the task.
133+
let _ = cancellable_background_tasks.tasks.spawn_on(
134+
async move {
135+
tokio::select! {
136+
biased;
137+
_ = cancellation_token.cancelled() => {},
138+
_ = future => {},
139+
}
140+
},
141+
runtime_handle,
142+
);
126143
}
127144

128145
pub fn allow_cancellable_background_task_spawns(&self) {
129-
self.cancellable_background_tasks.lock().expect("lock").accepting_tasks = true;
146+
let mut cancellable_background_tasks =
147+
self.cancellable_background_tasks.lock().expect("lock");
148+
if cancellable_background_tasks.cancellation_token.is_cancelled() {
149+
debug_assert!(
150+
cancellable_background_tasks.tasks.is_empty(),
151+
"Expected all cancellable background tasks to be stopped"
152+
);
153+
cancellable_background_tasks.cancellation_token = CancellationToken::new();
154+
}
155+
cancellable_background_tasks.tasks.reopen();
156+
cancellable_background_tasks.accepting_tasks = true;
130157
}
131158

132159
pub fn spawn_background_processor_task<F>(&self, future: F)
@@ -164,15 +191,15 @@ impl Runtime {
164191
}
165192

166193
pub fn abort_cancellable_background_tasks(&self) {
167-
let mut tasks = {
194+
let tasks = {
168195
let mut cancellable_background_tasks =
169196
self.cancellable_background_tasks.lock().expect("lock");
170197
cancellable_background_tasks.accepting_tasks = false;
171-
core::mem::take(&mut cancellable_background_tasks.tasks)
198+
cancellable_background_tasks.tasks.close();
199+
cancellable_background_tasks.cancellation_token.cancel();
200+
cancellable_background_tasks.tasks.clone()
172201
};
173-
debug_assert!(tasks.len() > 0, "Expected some cancellable background_tasks");
174-
tasks.abort_all();
175-
self.block_on(async { while let Some(_) = tasks.join_next().await {} })
202+
self.block_on(tasks.wait())
176203
}
177204

178205
pub fn wait_on_background_tasks(&self) {
@@ -381,19 +408,68 @@ impl FutureSpawner for RuntimeSpawner {
381408

382409
#[cfg(test)]
383410
mod tests {
384-
use tokio::sync::oneshot;
411+
use tokio::sync::{mpsc, oneshot};
385412

386413
use super::*;
387414

415+
struct DropNotifier(Option<oneshot::Sender<()>>);
416+
417+
impl Drop for DropNotifier {
418+
fn drop(&mut self) {
419+
if let Some(sender) = self.0.take() {
420+
let _ = sender.send(());
421+
}
422+
}
423+
}
424+
388425
fn test_runtime() -> Runtime {
389426
Runtime::new(Arc::new(Logger::new_log_facade())).unwrap()
390427
}
391428

429+
#[test]
430+
fn completed_cancellable_tasks_are_released_before_shutdown() {
431+
const TASK_COUNT: usize = 64;
432+
433+
let runtime = test_runtime();
434+
let (completion_sender, mut completion_receiver) = mpsc::channel(TASK_COUNT);
435+
for _ in 0..TASK_COUNT {
436+
let completion_sender = completion_sender.clone();
437+
runtime.spawn_cancellable_background_task(async move {
438+
completion_sender.send(()).await.expect("completion receiver should be open");
439+
});
440+
}
441+
drop(completion_sender);
442+
443+
let completed_tasks_are_released = runtime.block_on(async {
444+
for _ in 0..TASK_COUNT {
445+
completion_receiver.recv().await.expect("cancellable task should complete");
446+
}
447+
448+
tokio::time::timeout(Duration::from_secs(1), async {
449+
loop {
450+
if runtime.cancellable_background_tasks.lock().expect("lock").tasks.is_empty() {
451+
break;
452+
}
453+
tokio::task::yield_now().await;
454+
}
455+
})
456+
.await
457+
.is_ok()
458+
});
459+
460+
assert!(
461+
completed_tasks_are_released,
462+
"completed cancellable tasks should be released before shutdown"
463+
);
464+
}
465+
392466
#[test]
393467
fn late_cancellable_spawns_are_not_polled_after_abort() {
394468
let runtime = test_runtime();
395469
let (started_sender, started_receiver) = oneshot::channel();
470+
let (dropped_sender, dropped_receiver) = oneshot::channel();
396471
runtime.spawn_cancellable_background_task(async move {
472+
let _drop_notifier = DropNotifier(Some(dropped_sender));
397473
let _ = started_sender.send(());
398474
std::future::pending::<()>().await;
399475
});
@@ -402,6 +478,9 @@ mod tests {
402478
});
403479

404480
runtime.abort_cancellable_background_tasks();
481+
runtime.block_on(async {
482+
dropped_receiver.await.expect("aborted task should be dropped before abort returns");
483+
});
405484

406485
let (late_spawn_sender, late_spawn_receiver) = oneshot::channel();
407486
runtime.spawn_cancellable_background_task(async move {

0 commit comments

Comments
 (0)