Release completed cancellable tasks - #997
Conversation
|
👋 Thanks for assigning @tankyleo as a reviewer! |
3553a89 to
fb5fbc8
Compare
|
Rebased after #956 landed. |
elnosh
left a comment
There was a problem hiding this comment.
ACK fix in 08d3b5d
I'm less familiar with the code in some of the other commits, specially in liquidity. Some of the changes implementing the different Guards took me quite a bit to understand so I would've appreciated some comments but they look correct. Specifically:
- reason for
PendingRequestGuardneeding the tokens for the pending requests inliquidity/mod.rs PendingRequestinliquidityseparating the ownerSenderfrom the otherfollowersVec<oneshot::Sender<T>>. Inconnection.rsthere's just one vec for all the senders.
These make sense now but not particularly obvious so adding comments is just a suggestion, I'm fine with it as-is
There was a problem hiding this comment.
similar to fb5fbc8 this debug_assert could be removed as receiver from this pending connection attempt might have been dropped.
tankyleo
left a comment
There was a problem hiding this comment.
So far just looked at the first commit, will continue tomorrow
| tasks: JoinSet<()>, | ||
| tasks: TaskTracker, | ||
| cancellation_token: CancellationToken, | ||
| accepting_tasks: bool, |
There was a problem hiding this comment.
We can delete this field here, and track is_closed on TaskTracker instead
| let mut cancellable_background_tasks = | ||
| self.cancellable_background_tasks.lock().expect("lock"); | ||
| if cancellable_background_tasks.cancellation_token.is_cancelled() { | ||
| debug_assert!( |
There was a problem hiding this comment.
I believe this debug_assert is reachable here: further below we cancel the token, then drop the lock, then wait. So here the token could be canceled, but the tasks not yet empty.
There was a problem hiding this comment.
Hmm, it's def. not reachable from the current callsites as they are serialized.
There was a problem hiding this comment.
Agreed the current callsites are serialized. Might be good to not rely on this external lock for serialization, I vibed with codex on this issue, here's the patch it came up with below. Your call if you want to incorporate this or leave the PR as is :)
Isolate cancellable task generations
Replace cancelled cancellable-task state as a unit so a restart cannot reopen the tracker being drained by an in-flight abort.
AI-Assisted-By: OpenAI Codex
diff --git a/src/runtime.rs b/src/runtime.rs
index d4ea7ce..3fe04d1 100644
--- a/src/runtime.rs
+++ b/src/runtime.rs
@@ -146,14 +146,10 @@ impl Runtime {
let mut cancellable_background_tasks =
self.cancellable_background_tasks.lock().expect("lock");
if cancellable_background_tasks.cancellation_token.is_cancelled() {
- debug_assert!(
- cancellable_background_tasks.tasks.is_empty(),
- "Expected all cancellable background tasks to be stopped"
- );
- cancellable_background_tasks.cancellation_token = CancellationToken::new();
+ // An abort may still be waiting on a clone of the previous task tracker. Start a new
+ // generation instead of reopening that tracker underneath the in-flight wait.
+ *cancellable_background_tasks = CancellableBackgroundTasks::new();
}
- cancellable_background_tasks.tasks.reopen();
- cancellable_background_tasks.accepting_tasks = true;
}
pub fn spawn_background_processor_task<F>(&self, future: F)| debug_assert!(tasks.len() > 0, "Expected some cancellable background_tasks"); | ||
| tasks.abort_all(); | ||
| self.block_on(async { while let Some(_) = tasks.join_next().await {} }) | ||
| self.block_on(tasks.wait()) |
There was a problem hiding this comment.
IIRC while we are waiting on tasks.wait, we could call allow_cancellable_background_task_spawns, reopen the TaskTracker, and thus tasks.wait() would never finish.
There was a problem hiding this comment.
Hmm, yes, if we only look at Runtime alone, but note that we are always acquiring the is_running lock in start/stop. Let me know if you think it's crucial to make Runtime more robust by itself.
There was a problem hiding this comment.
let me know what you think of the patch above, it would address this here too.
| Self { pending_connections, node_id, active: true } | ||
| } | ||
|
|
||
| fn disarm(&mut self) { |
There was a problem hiding this comment.
nit: Is this really needed ? On drop, we'd briefly lock the pending connections, see that we have no subscribers for the node_id, and do nothing.
There was a problem hiding this comment.
Yes. Although result propagation normally removes the entry, another thread could register a replacement after the mutex is released but before this guard drops. Disarming prevents the old guard from removing that new attempt.
| drop(connection_guard); | ||
|
|
||
| assert!( | ||
| pending_connections.lock().expect("lock").is_empty(), |
There was a problem hiding this comment.
nit: Should we also make sure that key-values for a different node_id are not dropped ?
There was a problem hiding this comment.
I’d prefer to leave this out since removal is explicitly scoped by node_id, and the existing test covers the relevant cancellation behavior.
| drop(request_guard); | ||
|
|
||
| assert!( | ||
| pending_requests.lock().expect("lock").is_empty(), |
There was a problem hiding this comment.
Similar here perhaps some coverage to make sure we don't drop requests with a different request key.
There was a problem hiding this comment.
See above, would also prefer to leave it out for now.
Refills ran on the joined background task set, which shutdown waits on — a refill wedged on an unresponsive store holds up every stop for the per-task timeout. Spawn them on the cancellable set instead, which the node aborts at shutdown. Completed tasks still accumulate there until shutdown; the runtime rework in lightningdevkit#997 is what reaps them continuously. Aborting a task mid-refill must not lose wallet state: the refill used to hold the taken change set in a local across its store writes, so an abort landing there dropped reveals that were already taken from the wallet's staged state — a later refill would then publish addresses no persisted wallet state covers, recreating the unwatched-script problem the pool exists to prevent. The refill now stages the taken change set with the persister in the same critical section that takes it from the wallet, so an abort at any await leaves the reveals pending for the next persist call to flush. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fb5fbc8 to
a4461ac
Compare
Avoid retaining completed Tokio task allocations for the node lifetime while preserving shutdown cancellation and restart semantics. Co-Authored-By: HAL 9000
Check the tracker directly instead of duplicating spawn state. This keeps task admission aligned with shutdown tracking. Co-Authored-By: HAL 9000
Replace closed task state as a unit so a restart cannot reopen the tracker being drained by an in-flight shutdown. Co-Authored-By: HAL 9000
Signal cancellation when cancellable task state is dropped so detached tasks cannot outlive a node that fails during startup. Co-Authored-By: HAL 9000
Remove pending LSPS request state when callers time out or are cancelled so unresponsive services cannot grow request maps. Co-Authored-By: HAL 9000
Treat responses for expired requests as expected so delayed LSP messages do not panic the liquidity event loop. Co-Authored-By: HAL 9000
Clear per-peer connection state when the leading task is cancelled so later callers can retry and existing subscribers do not hang. Co-Authored-By: HAL 9000
Connection subscribers may cancel before a shared attempt finishes. Treat the failed send as expected instead of panicking in debug builds. Co-Authored-By: HAL 9000
Restore wallet sync status and notify waiting callers when the task performing a sync is cancelled, allowing later sync attempts to run. Co-Authored-By: HAL 9000
Wait for an active manual sync and retry initial ownership so startup races do not permanently stop background polling. Co-Authored-By: HAL 9000
a4461ac to
2c1682e
Compare
|
Now updated to address pending feedback. This is now great quite a bit in scope, which I'm not super happy with. But let me know what you think. |
Avoid retaining completed Tokio task allocations for the node lifetime while preserving shutdown cancellation and restart semantics. This fixes a considerable memory leak as previously we'd retain the
JoinSeted task's allocations untilstop/join_next, as tokio thankfully only notes in the https://docs.rs/tokio-util/latest/tokio_util/task/task_tracker/struct.TaskTracker.html#comparison-to-joinset.Thanks to @elnosh for reporting this leak.