Skip to content

Introduce Runtime object allowng to detect outer runtime context - #543

Merged
tnull merged 1 commit into
lightningdevkit:mainfrom
tnull:2025-05-allow-to-use-runtime-handle
Aug 18, 2025
Merged

Introduce Runtime object allowng to detect outer runtime context#543
tnull merged 1 commit into
lightningdevkit:mainfrom
tnull:2025-05-allow-to-use-runtime-handle

Conversation

@tnull

@tnulltnull commented May 19, 2025

Copy link
Copy Markdown
Collaborator

Closes#491

Instead of holding an Arc<RwLock<Option<Arc<tokio::runtime::Runtime>>>
and dealing with stuff like tokio::task::block_in_place at all
callsites, we introduce a Runtime object that takes care of the state
transitions, and allows to detect and reuse an outer runtime context.

We also adjust the with_runtime API to take a tokio::runtime::Handle
rather than an Arc<Runtime>.

We then also go ahead an reuse said Runtime object for VssStore.

(cc @andrei-21)

@ldk-reviews-bot

ldk-reviews-bot commented May 19, 2025

Copy link
Copy Markdown

👋 Thanks for assigning @TheBlueMatt as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@tnull
tnull marked this pull request as draft May 19, 2025 15:10
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from d42f762 to 2f43096CompareMay 22, 2025 09:27
@tnulltnull changed the title Introduce Runtime object and allow to take a tokio::runtime::HandleIntroduce Runtime object allowng to detect outer runtime contextMay 22, 2025
@tnull
tnull marked this pull request as ready for review May 22, 2025 09:29
Comment threadsrc/chain/electrum.rs Outdated
Comment threadsrc/runtime.rs Outdated
Comment threadsrc/runtime.rs Outdated
Comment threadsrc/chain/electrum.rs Outdated
Comment threadsrc/chain/electrum.rs Outdated
Comment threadsrc/runtime.rs Outdated
Comment threadsrc/runtime.rs Outdated

@andrei-21andrei-21 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have tested with my prototype, everything works ok.

Comment threadsrc/runtime.rs Outdated
Comment threadsrc/runtime.rs Outdated

pub fn block_on<F: Future>(&self, future: F) -> Result<F::Output, RuntimeError> {
let handle = self.handle()?;
Ok(tokio::task::block_in_place(move || handle.block_on(future)))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After our offline chat, I was looking up the code for block_in_place: https://github.com/tokio-rs/tokio/blob/17d8c2b29d94550f504d8fd76d8d8aaf66095864/tokio/src/runtime/scheduler/multi_thread/worker.rs#L358

It indeed uses a thread local context inside.

If you use this anyway, isn't the only correct way to always also use tokio::runtime::Handle::try_current() ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We spoke about it, but seeing this again it feels a bit undefined to mix things in this non-transparent way.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mentioned the case where there is no current runtime though. But that is detectable too, and in that case the call doesn't need to be wrapped in block_on?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code comments exactly explaining why this block_in_place -> block_on chaining is needed would be helpful too.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, please take a look at the approach I just pushed: now went with preferring the Handle/spawned runtime everywhere, except in block_on where the outer context will take precedence.

Comment threadsrc/runtime.rs Outdated
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from 2f43096 to 2f32e15CompareMay 23, 2025 11:42
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Now pushed a new approach that initializes the Runtime in the builder, which allows to clean up a lot of the error cases. Kinda makes sense to go this way, as starting/stopping the runtime is out of our control anyways, if the user gives us a handle to use.

@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch 3 times, most recently from 3d7c8a6 to 48a42f1CompareMay 23, 2025 12:00

@joostjagerjoostjager left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice clean up of the error cases indeed.

Comment threadsrc/runtime.rs
Comment threadsrc/runtime.rs Outdated
// during `block_on`, as this is the context `block_in_place` would operate on. So we try
// to detect the outer context here, and otherwise use whatever was set during
// initialization.
let handle = tokio::runtime::Handle::try_current().unwrap_or(self.handle());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is used here, shouldn't it be used everywhere (spawn, spawn_blocking)?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm very confused: below you argue against invisible capture, here you say we should invisibly capture the context from any entry point? No, I think generally using what was set on startup and only making an exception for block_on makes sense.

@joostjagerjoostjagerMay 23, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not the same. In new the handle is saved and there's the implicit requirement for it to stay alive.

Here it isn't saved. It's just used if present, and otherwise we fall back to what the user configured and knows they need to keep alive.

Why make an exception for block_on? I'd think it is better to be consistent, and if it can't be avoid in block_on it should be done everywhere?

Comment threadsrc/runtime.rs
impl Runtime {
pub fn new() -> Result<Self, std::io::Error> {
let mode = match tokio::runtime::Handle::try_current() {
Ok(handle) => RuntimeMode::Handle(handle),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It still makes me feel a bit uncomfortable that that handle is capture here invisibly, and that there's the implicit assumption that the user will keep this alive.

Removing it here, and letting the user pass it in themselves via the builder seems to be a more transparent way to signal that it is used and needs to remain available.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I see your concern, but I'm not sure. I think the current behavior is the expected default behavior that just makes it work transparently for any upstream users. And we need to auto-detect for block_on at the very least anyways. Not sure if @TheBlueMatt has an opinion here, since he (as a user) requested auto-detection in #491?

Comment threadsrc/builder.rs
log_error!(logger, "Failed to setup tokio runtime: {}", e);
BuildError::RuntimeSetupFailed
})?)
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code reuse opportunity

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NACK, IMO it's much more readable to keep short blocks like this inlined, instead of having the reader jump all over the file, losing context.

@joostjagerjoostjagerMay 23, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't agree. I think eliminating the risk of future code changes not being applied in both places is more important than having the reader jump to a function. The function can have a descriptive name too such as build_runtime. I don't think it is bad for readability at all.

Your argument of jumping all over the file would also apply to code that isn't necessarily duplicated. Because also in that case, the reader would have to jump. I think that would lead to long function bodies, of which there are already too many in ldk, and those absolutely do not help with readability.

Comment threadsrc/event.rs
Comment threadsrc/gossip.rs
Comment threadsrc/gossip.rs
}
}

impl FutureSpawner for RuntimeSpawner {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be implemented directly onto the new Runtime?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good thought, unfortunately no as GossipVerifier::new takes FutureSpawner by value, and of course we can't impl FutureSpawner for Arc<Runtime> as both sides of it would include non-local types.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there really no way to do this? Implement an interface on a local type and then pass it to the other crate? Or would it require a different type on the LDK side for FutureSpawner?

Perhaps longer term moving Runtime to rust-lightning could be a direction too?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there really no way to do this? Implement an interface on a local type and then pass it to the other crate?

There is a way to do this, which is the newtype pattern, which is essentially what we have.

@tnulltnullMay 23, 2025

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or would it require a different type on the LDK side for FutureSpawner?

The easiest way to avoid this would be to have GossipVerifier::new take a Deref<Target = FutureSpawner> or Borrow<FutureSpawner>, but honestly a newtype isn't too bad in this case, IMO.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. I might, for the async kv store pr, use Deref<Target=FS> then already as a preparation. But ofc the wrapper is no big deal.

Comment threadsrc/lib.rs
@tnull
tnull requested a review from andrei-21May 23, 2025 12:34
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

I have tested with my prototype, everything works ok.

@andrei-21 Thanks! Mind reconfirming this works as expected with the new approach?

@tnulltnull left a comment

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, seems switching VssStore over to use the same runtime has the integration test hang. Will need some more debugging.

Comment threadsrc/lib.rs
pub fn disconnect(&self, counterparty_node_id: PublicKey) -> Result<(), Error> {
let rt_lock = self.runtime.read().unwrap();
if rt_lock.is_none() {
if !*self.is_running.read().unwrap() {

@joostjagerjoostjagerMay 23, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of scope for this PR, but curious: we talked about the type-state pattern for Runtime and that it may not be ideal. Could the pattern work for Node though? So start returning a type that exposes the method, and that type being consumed by stop.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we had considered that previously, too. Maybe, although I'm not sure if we want to force this pattern on our users, i.e., they'd need to create something akin to an enum wrapper that could hold variants StoppedNode/StartedNode, or would pipe through different versions of the node in their app, depending on the node state. Also, in the future we'd like to handle (re-)starting on persistence failure for the users, and if they hold a StartedNode object, there is really no way to force them to drop it.

@joostjagerjoostjagerMay 26, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I have no experience with the pattern. It looks pretty powerful in combination with Rust's type system. Don't know if the pipe through can be done with a wrapper around the state types. Restart could be a method on the StartedNode perhaps. Either way, this was just a side remark.

@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch 3 times, most recently from f65ad68 to 838c1bcCompareMay 23, 2025 13:17
@andrei-21

Copy link
Copy Markdown
Contributor

I have tested with my prototype, everything works ok.

@andrei-21 Thanks! Mind reconfirming this works as expected with the new approach?

Tried again, seems to work as expected.

@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Tried again, seems to work as expected.

Thanks again!

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @andrei-21! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch 8 times, most recently from 15f300b to 47207f5CompareMay 27, 2025 12:10
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from 47207f5 to b838720CompareJuly 7, 2025 07:43
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from b838720 to 636c357CompareAugust 14, 2025 13:26
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Rebased this on current main to make some progress finally. I'm tempted to punt on the VSS part of it, as we're about to get a 'real' async KVStore with #462, so dropped that commit for now.

@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from 636c357 to 56e7977CompareAugust 14, 2025 13:50
@tnull
tnull requested a review from TheBlueMattAugust 14, 2025 13:50
@tnulltnull self-assigned this Aug 14, 2025
@tnulltnull added the weekly goal Someone wants to land this this week label Aug 14, 2025
@tnulltnull moved this to Goal: Merge in Weekly GoalsAug 14, 2025
Instead of holding an `Arc<RwLock<Option<Arc<tokio::runtime::Runtime>>>`
and dealing with stuff like `tokio::task::block_in_place` at all
callsites, we introduce a `Runtime` object that takes care of the state
transitions, and allows to detect and reuse an outer runtime context.
We also adjust the `with_runtime` API to take a `tokio::runtime::Handle`
rather than an `Arc<Runtime>`.
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from 56e7977 to 4879002CompareAugust 15, 2025 13:38
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Rebased on main to resolve conflicts.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @TheBlueMatt! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diff itself looks correct. I didn't try to verify that there aren't missing places where we should check for is_running, but did leave some suggestions to improve test coverage marginally.

Comment threadsrc/lib.rs
self.logger,
"Active runtime tasks left prior to shutdown: {}",
metrics_runtime.metrics().active_tasks_count()
runtime_handle.metrics().active_tasks_count()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a debug assertion that its zero? It seems like it would be pretty easy to spawn a long-running task that loops forever, has a reference to the owned runtime, and prevents it from ever droping.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might not necessarily be 0, especially when reusing an outer runtime. Currently we still have some gossip verification and HTLC-forwarding tasks that aren't necessarily finished before this point, but the latter will def. go away with #462.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right but we really need to test that we don't have forever-running tasks which leak. As written it would be really easy for a bug to slip in that lets a task run forever and stop() leaves a handful of threads lying around. Maybe that means tests need to wait for gossip verification and HTLC forwarding tasks even if prod builds don't.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right but we really need to test that we don't have forever-running tasks which leak. As written it would be really easy for a bug to slip in that lets a task run forever and stop() leaves a handful of threads lying around.

Yes, that is a possibility. I guess we could move the newly-introduced background_tasks and cancellable_background_tasksJoinSets into Runtime, and then only expose spawn_background_task/spawn_cancellable_background_task methods, ensuring that whenever we spawn we track the JoinHandle and abort or await it on shutdown.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmm, that sounds like a much better approach than just trying to test it.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did it in #619

Comment threadsrc/payment/bolt11.rs
payment_store: Arc<PaymentStore>,
peer_store: Arc<PeerStore<Arc<Logger>>>,
config: Arc<Config>,
is_running: Arc<RwLock<bool>>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we have our own Runtime type, can we move this into the Runtime and then add a debug_assertion when spawning a new task that running is true? Seems like that would add some additional test coverage and simplify the diff somewhat.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See below: is_running is really a property of Node that (as of this PR) doesn't mean "Runtime is available" (which we now assume), but is also a reentrancy guard for runtime state transitions.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the Runtime (not tokio Runtime but rather the ldk-node Runtime) is also a "property of the Node", so why shouldn't "you can use the runtime" be in that. Again this really feels like it's missing a lot of debug assertions - bugs slipping in in the future here seems really likely.

Comment threadsrc/builder.rs
let background_tasks = Mutex::new(None);
let cancellable_background_tasks = Mutex::new(None);

let is_running = Arc::new(RwLock::new(false));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should really be an AtomicBool, not a RwLock<bool>.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we chose an RwLock for a reason, namely that we acquire and hold the lock during start/stop which keeps us from running into weird intermediate states if users would call start/stop in quick succession.

@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Landing this for now, might open a PR with minor follow-ups.

@tnull
tnull merged commit 110ab06 into lightningdevkit:mainAug 18, 2025
11 of 15 checks passed
@github-project-automationgithub-project-automationBot moved this from Goal: Merge to Done in Weekly GoalsAug 18, 2025
tnull added a commit to tnull/ldk-node that referenced this pull request Nov 3, 2025
We previously attempted to drop the internal runtime from `VssStore`,
resulting into blocking behavior. While we recently made changes that
improved our situation (having VSS CI pass again pretty reliably), we
just ran into yet another case where the VSS CI hung (cf.
https://github.com/lightningdevkit/vss-server/actions/runs/19023212819/job/54322173817?pr=59).
Here we attempt to restore even more of the original pre-
ab3d78d / lightningdevkit#543 behavior to get rid of
the reappearing blocking behavior, i.e., only use the internal runtime
in `VssStore`.
tnull added a commit to tnull/ldk-node that referenced this pull request Nov 3, 2025
We previously attempted to drop the internal runtime from `VssStore`,
resulting into blocking behavior. While we recently made changes that
improved our situation (having VSS CI pass again pretty reliably), we
just ran into yet another case where the VSS CI hung (cf.
https://github.com/lightningdevkit/vss-server/actions/runs/19023212819/job/54322173817?pr=59).
Here we attempt to restore even more of the original pre-
ab3d78d / lightningdevkit#543 behavior to get rid of
the reappearing blocking behavior, i.e., only use the internal runtime
in `VssStore`.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

weekly goalSomeone wants to land this this week

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Auto-detect existing tokio runtime?

5 participants

@tnull@ldk-reviews-bot@andrei-21@TheBlueMatt@joostjager
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Introduce `Runtime` object allowng to detect outer runtime context by tnull · Pull Request #543 · lightningdevkit/ldk-node · GitHub
Skip to content

Introduce Runtime object allowng to detect outer runtime context - #543

Merged
tnull merged 1 commit into
lightningdevkit:mainfrom
tnull:2025-05-allow-to-use-runtime-handle
Aug 18, 2025
Merged

Introduce Runtime object allowng to detect outer runtime context#543
tnull merged 1 commit into
lightningdevkit:mainfrom
tnull:2025-05-allow-to-use-runtime-handle

Conversation

@tnull

@tnulltnull commented May 19, 2025

Copy link
Copy Markdown
Collaborator

Closes#491

Instead of holding an Arc<RwLock<Option<Arc<tokio::runtime::Runtime>>>
and dealing with stuff like tokio::task::block_in_place at all
callsites, we introduce a Runtime object that takes care of the state
transitions, and allows to detect and reuse an outer runtime context.

We also adjust the with_runtime API to take a tokio::runtime::Handle
rather than an Arc<Runtime>.

We then also go ahead an reuse said Runtime object for VssStore.

(cc @andrei-21)

@ldk-reviews-bot

ldk-reviews-bot commented May 19, 2025

Copy link
Copy Markdown

👋 Thanks for assigning @TheBlueMatt as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@tnull
tnull marked this pull request as draft May 19, 2025 15:10
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from d42f762 to 2f43096CompareMay 22, 2025 09:27
@tnulltnull changed the title Introduce Runtime object and allow to take a tokio::runtime::HandleIntroduce Runtime object allowng to detect outer runtime contextMay 22, 2025
@tnull
tnull marked this pull request as ready for review May 22, 2025 09:29
Comment threadsrc/chain/electrum.rs Outdated
Comment threadsrc/runtime.rs Outdated
Comment threadsrc/runtime.rs Outdated
Comment threadsrc/chain/electrum.rs Outdated
Comment threadsrc/chain/electrum.rs Outdated
Comment threadsrc/runtime.rs Outdated
Comment threadsrc/runtime.rs Outdated

@andrei-21andrei-21 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have tested with my prototype, everything works ok.

Comment threadsrc/runtime.rs Outdated
Comment threadsrc/runtime.rs Outdated

pub fn block_on<F: Future>(&self, future: F) -> Result<F::Output, RuntimeError> {
let handle = self.handle()?;
Ok(tokio::task::block_in_place(move || handle.block_on(future)))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After our offline chat, I was looking up the code for block_in_place: https://github.com/tokio-rs/tokio/blob/17d8c2b29d94550f504d8fd76d8d8aaf66095864/tokio/src/runtime/scheduler/multi_thread/worker.rs#L358

It indeed uses a thread local context inside.

If you use this anyway, isn't the only correct way to always also use tokio::runtime::Handle::try_current() ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We spoke about it, but seeing this again it feels a bit undefined to mix things in this non-transparent way.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mentioned the case where there is no current runtime though. But that is detectable too, and in that case the call doesn't need to be wrapped in block_on?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code comments exactly explaining why this block_in_place -> block_on chaining is needed would be helpful too.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, please take a look at the approach I just pushed: now went with preferring the Handle/spawned runtime everywhere, except in block_on where the outer context will take precedence.

Comment threadsrc/runtime.rs Outdated
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from 2f43096 to 2f32e15CompareMay 23, 2025 11:42
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Now pushed a new approach that initializes the Runtime in the builder, which allows to clean up a lot of the error cases. Kinda makes sense to go this way, as starting/stopping the runtime is out of our control anyways, if the user gives us a handle to use.

@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch 3 times, most recently from 3d7c8a6 to 48a42f1CompareMay 23, 2025 12:00

@joostjagerjoostjager left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice clean up of the error cases indeed.

Comment threadsrc/runtime.rs
Comment threadsrc/runtime.rs Outdated
// during `block_on`, as this is the context `block_in_place` would operate on. So we try
// to detect the outer context here, and otherwise use whatever was set during
// initialization.
let handle = tokio::runtime::Handle::try_current().unwrap_or(self.handle());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is used here, shouldn't it be used everywhere (spawn, spawn_blocking)?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm very confused: below you argue against invisible capture, here you say we should invisibly capture the context from any entry point? No, I think generally using what was set on startup and only making an exception for block_on makes sense.

@joostjagerjoostjagerMay 23, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not the same. In new the handle is saved and there's the implicit requirement for it to stay alive.

Here it isn't saved. It's just used if present, and otherwise we fall back to what the user configured and knows they need to keep alive.

Why make an exception for block_on? I'd think it is better to be consistent, and if it can't be avoid in block_on it should be done everywhere?

Comment threadsrc/runtime.rs
impl Runtime {
pub fn new() -> Result<Self, std::io::Error> {
let mode = match tokio::runtime::Handle::try_current() {
Ok(handle) => RuntimeMode::Handle(handle),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It still makes me feel a bit uncomfortable that that handle is capture here invisibly, and that there's the implicit assumption that the user will keep this alive.

Removing it here, and letting the user pass it in themselves via the builder seems to be a more transparent way to signal that it is used and needs to remain available.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I see your concern, but I'm not sure. I think the current behavior is the expected default behavior that just makes it work transparently for any upstream users. And we need to auto-detect for block_on at the very least anyways. Not sure if @TheBlueMatt has an opinion here, since he (as a user) requested auto-detection in #491?

Comment threadsrc/builder.rs
log_error!(logger, "Failed to setup tokio runtime: {}", e);
BuildError::RuntimeSetupFailed
})?)
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code reuse opportunity

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NACK, IMO it's much more readable to keep short blocks like this inlined, instead of having the reader jump all over the file, losing context.

@joostjagerjoostjagerMay 23, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't agree. I think eliminating the risk of future code changes not being applied in both places is more important than having the reader jump to a function. The function can have a descriptive name too such as build_runtime. I don't think it is bad for readability at all.

Your argument of jumping all over the file would also apply to code that isn't necessarily duplicated. Because also in that case, the reader would have to jump. I think that would lead to long function bodies, of which there are already too many in ldk, and those absolutely do not help with readability.

Comment threadsrc/event.rs
Comment threadsrc/gossip.rs
Comment threadsrc/gossip.rs
}
}

impl FutureSpawner for RuntimeSpawner {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be implemented directly onto the new Runtime?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good thought, unfortunately no as GossipVerifier::new takes FutureSpawner by value, and of course we can't impl FutureSpawner for Arc<Runtime> as both sides of it would include non-local types.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there really no way to do this? Implement an interface on a local type and then pass it to the other crate? Or would it require a different type on the LDK side for FutureSpawner?

Perhaps longer term moving Runtime to rust-lightning could be a direction too?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there really no way to do this? Implement an interface on a local type and then pass it to the other crate?

There is a way to do this, which is the newtype pattern, which is essentially what we have.

@tnulltnullMay 23, 2025

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or would it require a different type on the LDK side for FutureSpawner?

The easiest way to avoid this would be to have GossipVerifier::new take a Deref<Target = FutureSpawner> or Borrow<FutureSpawner>, but honestly a newtype isn't too bad in this case, IMO.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. I might, for the async kv store pr, use Deref<Target=FS> then already as a preparation. But ofc the wrapper is no big deal.

Comment threadsrc/lib.rs
@tnull
tnull requested a review from andrei-21May 23, 2025 12:34
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

I have tested with my prototype, everything works ok.

@andrei-21 Thanks! Mind reconfirming this works as expected with the new approach?

@tnulltnull left a comment

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, seems switching VssStore over to use the same runtime has the integration test hang. Will need some more debugging.

Comment threadsrc/lib.rs
pub fn disconnect(&self, counterparty_node_id: PublicKey) -> Result<(), Error> {
let rt_lock = self.runtime.read().unwrap();
if rt_lock.is_none() {
if !*self.is_running.read().unwrap() {

@joostjagerjoostjagerMay 23, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of scope for this PR, but curious: we talked about the type-state pattern for Runtime and that it may not be ideal. Could the pattern work for Node though? So start returning a type that exposes the method, and that type being consumed by stop.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we had considered that previously, too. Maybe, although I'm not sure if we want to force this pattern on our users, i.e., they'd need to create something akin to an enum wrapper that could hold variants StoppedNode/StartedNode, or would pipe through different versions of the node in their app, depending on the node state. Also, in the future we'd like to handle (re-)starting on persistence failure for the users, and if they hold a StartedNode object, there is really no way to force them to drop it.

@joostjagerjoostjagerMay 26, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I have no experience with the pattern. It looks pretty powerful in combination with Rust's type system. Don't know if the pipe through can be done with a wrapper around the state types. Restart could be a method on the StartedNode perhaps. Either way, this was just a side remark.

@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch 3 times, most recently from f65ad68 to 838c1bcCompareMay 23, 2025 13:17
@andrei-21

Copy link
Copy Markdown
Contributor

I have tested with my prototype, everything works ok.

@andrei-21 Thanks! Mind reconfirming this works as expected with the new approach?

Tried again, seems to work as expected.

@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Tried again, seems to work as expected.

Thanks again!

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @andrei-21! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch 8 times, most recently from 15f300b to 47207f5CompareMay 27, 2025 12:10
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from 47207f5 to b838720CompareJuly 7, 2025 07:43
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from b838720 to 636c357CompareAugust 14, 2025 13:26
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Rebased this on current main to make some progress finally. I'm tempted to punt on the VSS part of it, as we're about to get a 'real' async KVStore with #462, so dropped that commit for now.

@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from 636c357 to 56e7977CompareAugust 14, 2025 13:50
@tnull
tnull requested a review from TheBlueMattAugust 14, 2025 13:50
@tnulltnull self-assigned this Aug 14, 2025
@tnulltnull added the weekly goal Someone wants to land this this week label Aug 14, 2025
@tnulltnull moved this to Goal: Merge in Weekly GoalsAug 14, 2025
Instead of holding an `Arc<RwLock<Option<Arc<tokio::runtime::Runtime>>>`
and dealing with stuff like `tokio::task::block_in_place` at all
callsites, we introduce a `Runtime` object that takes care of the state
transitions, and allows to detect and reuse an outer runtime context.
We also adjust the `with_runtime` API to take a `tokio::runtime::Handle`
rather than an `Arc<Runtime>`.
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from 56e7977 to 4879002CompareAugust 15, 2025 13:38
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Rebased on main to resolve conflicts.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @TheBlueMatt! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diff itself looks correct. I didn't try to verify that there aren't missing places where we should check for is_running, but did leave some suggestions to improve test coverage marginally.

Comment threadsrc/lib.rs
self.logger,
"Active runtime tasks left prior to shutdown: {}",
metrics_runtime.metrics().active_tasks_count()
runtime_handle.metrics().active_tasks_count()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a debug assertion that its zero? It seems like it would be pretty easy to spawn a long-running task that loops forever, has a reference to the owned runtime, and prevents it from ever droping.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might not necessarily be 0, especially when reusing an outer runtime. Currently we still have some gossip verification and HTLC-forwarding tasks that aren't necessarily finished before this point, but the latter will def. go away with #462.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right but we really need to test that we don't have forever-running tasks which leak. As written it would be really easy for a bug to slip in that lets a task run forever and stop() leaves a handful of threads lying around. Maybe that means tests need to wait for gossip verification and HTLC forwarding tasks even if prod builds don't.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right but we really need to test that we don't have forever-running tasks which leak. As written it would be really easy for a bug to slip in that lets a task run forever and stop() leaves a handful of threads lying around.

Yes, that is a possibility. I guess we could move the newly-introduced background_tasks and cancellable_background_tasksJoinSets into Runtime, and then only expose spawn_background_task/spawn_cancellable_background_task methods, ensuring that whenever we spawn we track the JoinHandle and abort or await it on shutdown.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmm, that sounds like a much better approach than just trying to test it.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did it in #619

Comment threadsrc/payment/bolt11.rs
payment_store: Arc<PaymentStore>,
peer_store: Arc<PeerStore<Arc<Logger>>>,
config: Arc<Config>,
is_running: Arc<RwLock<bool>>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we have our own Runtime type, can we move this into the Runtime and then add a debug_assertion when spawning a new task that running is true? Seems like that would add some additional test coverage and simplify the diff somewhat.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See below: is_running is really a property of Node that (as of this PR) doesn't mean "Runtime is available" (which we now assume), but is also a reentrancy guard for runtime state transitions.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the Runtime (not tokio Runtime but rather the ldk-node Runtime) is also a "property of the Node", so why shouldn't "you can use the runtime" be in that. Again this really feels like it's missing a lot of debug assertions - bugs slipping in in the future here seems really likely.

Comment threadsrc/builder.rs
let background_tasks = Mutex::new(None);
let cancellable_background_tasks = Mutex::new(None);

let is_running = Arc::new(RwLock::new(false));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should really be an AtomicBool, not a RwLock<bool>.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we chose an RwLock for a reason, namely that we acquire and hold the lock during start/stop which keeps us from running into weird intermediate states if users would call start/stop in quick succession.

@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Landing this for now, might open a PR with minor follow-ups.

@tnull
tnull merged commit 110ab06 into lightningdevkit:mainAug 18, 2025
11 of 15 checks passed
@github-project-automationgithub-project-automationBot moved this from Goal: Merge to Done in Weekly GoalsAug 18, 2025
tnull added a commit to tnull/ldk-node that referenced this pull request Nov 3, 2025
We previously attempted to drop the internal runtime from `VssStore`,
resulting into blocking behavior. While we recently made changes that
improved our situation (having VSS CI pass again pretty reliably), we
just ran into yet another case where the VSS CI hung (cf.
https://github.com/lightningdevkit/vss-server/actions/runs/19023212819/job/54322173817?pr=59).
Here we attempt to restore even more of the original pre-
ab3d78d / lightningdevkit#543 behavior to get rid of
the reappearing blocking behavior, i.e., only use the internal runtime
in `VssStore`.
tnull added a commit to tnull/ldk-node that referenced this pull request Nov 3, 2025
We previously attempted to drop the internal runtime from `VssStore`,
resulting into blocking behavior. While we recently made changes that
improved our situation (having VSS CI pass again pretty reliably), we
just ran into yet another case where the VSS CI hung (cf.
https://github.com/lightningdevkit/vss-server/actions/runs/19023212819/job/54322173817?pr=59).
Here we attempt to restore even more of the original pre-
ab3d78d / lightningdevkit#543 behavior to get rid of
the reappearing blocking behavior, i.e., only use the internal runtime
in `VssStore`.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

weekly goalSomeone wants to land this this week

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Auto-detect existing tokio runtime?

5 participants

@tnull@ldk-reviews-bot@andrei-21@TheBlueMatt@joostjager
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Introduce `Runtime` object allowng to detect outer runtime context by tnull · Pull Request #543 · lightningdevkit/ldk-node · GitHub
Skip to content

Introduce Runtime object allowng to detect outer runtime context - #543

Merged
tnull merged 1 commit into
lightningdevkit:mainfrom
tnull:2025-05-allow-to-use-runtime-handle
Aug 18, 2025
Merged

Introduce Runtime object allowng to detect outer runtime context#543
tnull merged 1 commit into
lightningdevkit:mainfrom
tnull:2025-05-allow-to-use-runtime-handle

Conversation

@tnull

@tnulltnull commented May 19, 2025

Copy link
Copy Markdown
Collaborator

Closes#491

Instead of holding an Arc<RwLock<Option<Arc<tokio::runtime::Runtime>>>
and dealing with stuff like tokio::task::block_in_place at all
callsites, we introduce a Runtime object that takes care of the state
transitions, and allows to detect and reuse an outer runtime context.

We also adjust the with_runtime API to take a tokio::runtime::Handle
rather than an Arc<Runtime>.

We then also go ahead an reuse said Runtime object for VssStore.

(cc @andrei-21)

@ldk-reviews-bot

ldk-reviews-bot commented May 19, 2025

Copy link
Copy Markdown

👋 Thanks for assigning @TheBlueMatt as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@tnull
tnull marked this pull request as draft May 19, 2025 15:10
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from d42f762 to 2f43096CompareMay 22, 2025 09:27
@tnulltnull changed the title Introduce Runtime object and allow to take a tokio::runtime::HandleIntroduce Runtime object allowng to detect outer runtime contextMay 22, 2025
@tnull
tnull marked this pull request as ready for review May 22, 2025 09:29
Comment threadsrc/chain/electrum.rs Outdated
Comment threadsrc/runtime.rs Outdated
Comment threadsrc/runtime.rs Outdated
Comment threadsrc/chain/electrum.rs Outdated
Comment threadsrc/chain/electrum.rs Outdated
Comment threadsrc/runtime.rs Outdated
Comment threadsrc/runtime.rs Outdated

@andrei-21andrei-21 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have tested with my prototype, everything works ok.

Comment threadsrc/runtime.rs Outdated
Comment threadsrc/runtime.rs Outdated

pub fn block_on<F: Future>(&self, future: F) -> Result<F::Output, RuntimeError> {
let handle = self.handle()?;
Ok(tokio::task::block_in_place(move || handle.block_on(future)))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After our offline chat, I was looking up the code for block_in_place: https://github.com/tokio-rs/tokio/blob/17d8c2b29d94550f504d8fd76d8d8aaf66095864/tokio/src/runtime/scheduler/multi_thread/worker.rs#L358

It indeed uses a thread local context inside.

If you use this anyway, isn't the only correct way to always also use tokio::runtime::Handle::try_current() ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We spoke about it, but seeing this again it feels a bit undefined to mix things in this non-transparent way.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mentioned the case where there is no current runtime though. But that is detectable too, and in that case the call doesn't need to be wrapped in block_on?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code comments exactly explaining why this block_in_place -> block_on chaining is needed would be helpful too.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, please take a look at the approach I just pushed: now went with preferring the Handle/spawned runtime everywhere, except in block_on where the outer context will take precedence.

Comment threadsrc/runtime.rs Outdated
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from 2f43096 to 2f32e15CompareMay 23, 2025 11:42
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Now pushed a new approach that initializes the Runtime in the builder, which allows to clean up a lot of the error cases. Kinda makes sense to go this way, as starting/stopping the runtime is out of our control anyways, if the user gives us a handle to use.

@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch 3 times, most recently from 3d7c8a6 to 48a42f1CompareMay 23, 2025 12:00

@joostjagerjoostjager left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice clean up of the error cases indeed.

Comment threadsrc/runtime.rs
Comment threadsrc/runtime.rs Outdated
// during `block_on`, as this is the context `block_in_place` would operate on. So we try
// to detect the outer context here, and otherwise use whatever was set during
// initialization.
let handle = tokio::runtime::Handle::try_current().unwrap_or(self.handle());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is used here, shouldn't it be used everywhere (spawn, spawn_blocking)?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm very confused: below you argue against invisible capture, here you say we should invisibly capture the context from any entry point? No, I think generally using what was set on startup and only making an exception for block_on makes sense.

@joostjagerjoostjagerMay 23, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not the same. In new the handle is saved and there's the implicit requirement for it to stay alive.

Here it isn't saved. It's just used if present, and otherwise we fall back to what the user configured and knows they need to keep alive.

Why make an exception for block_on? I'd think it is better to be consistent, and if it can't be avoid in block_on it should be done everywhere?

Comment threadsrc/runtime.rs
impl Runtime {
pub fn new() -> Result<Self, std::io::Error> {
let mode = match tokio::runtime::Handle::try_current() {
Ok(handle) => RuntimeMode::Handle(handle),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It still makes me feel a bit uncomfortable that that handle is capture here invisibly, and that there's the implicit assumption that the user will keep this alive.

Removing it here, and letting the user pass it in themselves via the builder seems to be a more transparent way to signal that it is used and needs to remain available.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I see your concern, but I'm not sure. I think the current behavior is the expected default behavior that just makes it work transparently for any upstream users. And we need to auto-detect for block_on at the very least anyways. Not sure if @TheBlueMatt has an opinion here, since he (as a user) requested auto-detection in #491?

Comment threadsrc/builder.rs
log_error!(logger, "Failed to setup tokio runtime: {}", e);
BuildError::RuntimeSetupFailed
})?)
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code reuse opportunity

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NACK, IMO it's much more readable to keep short blocks like this inlined, instead of having the reader jump all over the file, losing context.

@joostjagerjoostjagerMay 23, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't agree. I think eliminating the risk of future code changes not being applied in both places is more important than having the reader jump to a function. The function can have a descriptive name too such as build_runtime. I don't think it is bad for readability at all.

Your argument of jumping all over the file would also apply to code that isn't necessarily duplicated. Because also in that case, the reader would have to jump. I think that would lead to long function bodies, of which there are already too many in ldk, and those absolutely do not help with readability.

Comment threadsrc/event.rs
Comment threadsrc/gossip.rs
Comment threadsrc/gossip.rs
}
}

impl FutureSpawner for RuntimeSpawner {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be implemented directly onto the new Runtime?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good thought, unfortunately no as GossipVerifier::new takes FutureSpawner by value, and of course we can't impl FutureSpawner for Arc<Runtime> as both sides of it would include non-local types.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there really no way to do this? Implement an interface on a local type and then pass it to the other crate? Or would it require a different type on the LDK side for FutureSpawner?

Perhaps longer term moving Runtime to rust-lightning could be a direction too?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there really no way to do this? Implement an interface on a local type and then pass it to the other crate?

There is a way to do this, which is the newtype pattern, which is essentially what we have.

@tnulltnullMay 23, 2025

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or would it require a different type on the LDK side for FutureSpawner?

The easiest way to avoid this would be to have GossipVerifier::new take a Deref<Target = FutureSpawner> or Borrow<FutureSpawner>, but honestly a newtype isn't too bad in this case, IMO.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. I might, for the async kv store pr, use Deref<Target=FS> then already as a preparation. But ofc the wrapper is no big deal.

Comment threadsrc/lib.rs
@tnull
tnull requested a review from andrei-21May 23, 2025 12:34
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

I have tested with my prototype, everything works ok.

@andrei-21 Thanks! Mind reconfirming this works as expected with the new approach?

@tnulltnull left a comment

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, seems switching VssStore over to use the same runtime has the integration test hang. Will need some more debugging.

Comment threadsrc/lib.rs
pub fn disconnect(&self, counterparty_node_id: PublicKey) -> Result<(), Error> {
let rt_lock = self.runtime.read().unwrap();
if rt_lock.is_none() {
if !*self.is_running.read().unwrap() {

@joostjagerjoostjagerMay 23, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of scope for this PR, but curious: we talked about the type-state pattern for Runtime and that it may not be ideal. Could the pattern work for Node though? So start returning a type that exposes the method, and that type being consumed by stop.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we had considered that previously, too. Maybe, although I'm not sure if we want to force this pattern on our users, i.e., they'd need to create something akin to an enum wrapper that could hold variants StoppedNode/StartedNode, or would pipe through different versions of the node in their app, depending on the node state. Also, in the future we'd like to handle (re-)starting on persistence failure for the users, and if they hold a StartedNode object, there is really no way to force them to drop it.

@joostjagerjoostjagerMay 26, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I have no experience with the pattern. It looks pretty powerful in combination with Rust's type system. Don't know if the pipe through can be done with a wrapper around the state types. Restart could be a method on the StartedNode perhaps. Either way, this was just a side remark.

@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch 3 times, most recently from f65ad68 to 838c1bcCompareMay 23, 2025 13:17
@andrei-21

Copy link
Copy Markdown
Contributor

I have tested with my prototype, everything works ok.

@andrei-21 Thanks! Mind reconfirming this works as expected with the new approach?

Tried again, seems to work as expected.

@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Tried again, seems to work as expected.

Thanks again!

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @andrei-21! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch 8 times, most recently from 15f300b to 47207f5CompareMay 27, 2025 12:10
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from 47207f5 to b838720CompareJuly 7, 2025 07:43
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from b838720 to 636c357CompareAugust 14, 2025 13:26
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Rebased this on current main to make some progress finally. I'm tempted to punt on the VSS part of it, as we're about to get a 'real' async KVStore with #462, so dropped that commit for now.

@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from 636c357 to 56e7977CompareAugust 14, 2025 13:50
@tnull
tnull requested a review from TheBlueMattAugust 14, 2025 13:50
@tnulltnull self-assigned this Aug 14, 2025
@tnulltnull added the weekly goal Someone wants to land this this week label Aug 14, 2025
@tnulltnull moved this to Goal: Merge in Weekly GoalsAug 14, 2025
Instead of holding an `Arc<RwLock<Option<Arc<tokio::runtime::Runtime>>>`
and dealing with stuff like `tokio::task::block_in_place` at all
callsites, we introduce a `Runtime` object that takes care of the state
transitions, and allows to detect and reuse an outer runtime context.
We also adjust the `with_runtime` API to take a `tokio::runtime::Handle`
rather than an `Arc<Runtime>`.
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from 56e7977 to 4879002CompareAugust 15, 2025 13:38
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Rebased on main to resolve conflicts.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @TheBlueMatt! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diff itself looks correct. I didn't try to verify that there aren't missing places where we should check for is_running, but did leave some suggestions to improve test coverage marginally.

Comment threadsrc/lib.rs
self.logger,
"Active runtime tasks left prior to shutdown: {}",
metrics_runtime.metrics().active_tasks_count()
runtime_handle.metrics().active_tasks_count()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a debug assertion that its zero? It seems like it would be pretty easy to spawn a long-running task that loops forever, has a reference to the owned runtime, and prevents it from ever droping.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might not necessarily be 0, especially when reusing an outer runtime. Currently we still have some gossip verification and HTLC-forwarding tasks that aren't necessarily finished before this point, but the latter will def. go away with #462.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right but we really need to test that we don't have forever-running tasks which leak. As written it would be really easy for a bug to slip in that lets a task run forever and stop() leaves a handful of threads lying around. Maybe that means tests need to wait for gossip verification and HTLC forwarding tasks even if prod builds don't.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right but we really need to test that we don't have forever-running tasks which leak. As written it would be really easy for a bug to slip in that lets a task run forever and stop() leaves a handful of threads lying around.

Yes, that is a possibility. I guess we could move the newly-introduced background_tasks and cancellable_background_tasksJoinSets into Runtime, and then only expose spawn_background_task/spawn_cancellable_background_task methods, ensuring that whenever we spawn we track the JoinHandle and abort or await it on shutdown.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmm, that sounds like a much better approach than just trying to test it.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did it in #619

Comment threadsrc/payment/bolt11.rs
payment_store: Arc<PaymentStore>,
peer_store: Arc<PeerStore<Arc<Logger>>>,
config: Arc<Config>,
is_running: Arc<RwLock<bool>>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we have our own Runtime type, can we move this into the Runtime and then add a debug_assertion when spawning a new task that running is true? Seems like that would add some additional test coverage and simplify the diff somewhat.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See below: is_running is really a property of Node that (as of this PR) doesn't mean "Runtime is available" (which we now assume), but is also a reentrancy guard for runtime state transitions.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the Runtime (not tokio Runtime but rather the ldk-node Runtime) is also a "property of the Node", so why shouldn't "you can use the runtime" be in that. Again this really feels like it's missing a lot of debug assertions - bugs slipping in in the future here seems really likely.

Comment threadsrc/builder.rs
let background_tasks = Mutex::new(None);
let cancellable_background_tasks = Mutex::new(None);

let is_running = Arc::new(RwLock::new(false));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should really be an AtomicBool, not a RwLock<bool>.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we chose an RwLock for a reason, namely that we acquire and hold the lock during start/stop which keeps us from running into weird intermediate states if users would call start/stop in quick succession.

@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Landing this for now, might open a PR with minor follow-ups.

@tnull
tnull merged commit 110ab06 into lightningdevkit:mainAug 18, 2025
11 of 15 checks passed
@github-project-automationgithub-project-automationBot moved this from Goal: Merge to Done in Weekly GoalsAug 18, 2025
tnull added a commit to tnull/ldk-node that referenced this pull request Nov 3, 2025
We previously attempted to drop the internal runtime from `VssStore`,
resulting into blocking behavior. While we recently made changes that
improved our situation (having VSS CI pass again pretty reliably), we
just ran into yet another case where the VSS CI hung (cf.
https://github.com/lightningdevkit/vss-server/actions/runs/19023212819/job/54322173817?pr=59).
Here we attempt to restore even more of the original pre-
ab3d78d / lightningdevkit#543 behavior to get rid of
the reappearing blocking behavior, i.e., only use the internal runtime
in `VssStore`.
tnull added a commit to tnull/ldk-node that referenced this pull request Nov 3, 2025
We previously attempted to drop the internal runtime from `VssStore`,
resulting into blocking behavior. While we recently made changes that
improved our situation (having VSS CI pass again pretty reliably), we
just ran into yet another case where the VSS CI hung (cf.
https://github.com/lightningdevkit/vss-server/actions/runs/19023212819/job/54322173817?pr=59).
Here we attempt to restore even more of the original pre-
ab3d78d / lightningdevkit#543 behavior to get rid of
the reappearing blocking behavior, i.e., only use the internal runtime
in `VssStore`.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

weekly goalSomeone wants to land this this week

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Auto-detect existing tokio runtime?

5 participants

@tnull@ldk-reviews-bot@andrei-21@TheBlueMatt@joostjager
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Introduce `Runtime` object allowng to detect outer runtime context by tnull · Pull Request #543 · lightningdevkit/ldk-node · GitHub
Skip to content

Introduce Runtime object allowng to detect outer runtime context - #543

Merged
tnull merged 1 commit into
lightningdevkit:mainfrom
tnull:2025-05-allow-to-use-runtime-handle
Aug 18, 2025
Merged

Introduce Runtime object allowng to detect outer runtime context#543
tnull merged 1 commit into
lightningdevkit:mainfrom
tnull:2025-05-allow-to-use-runtime-handle

Conversation

@tnull

@tnulltnull commented May 19, 2025

Copy link
Copy Markdown
Collaborator

Closes#491

Instead of holding an Arc<RwLock<Option<Arc<tokio::runtime::Runtime>>>
and dealing with stuff like tokio::task::block_in_place at all
callsites, we introduce a Runtime object that takes care of the state
transitions, and allows to detect and reuse an outer runtime context.

We also adjust the with_runtime API to take a tokio::runtime::Handle
rather than an Arc<Runtime>.

We then also go ahead an reuse said Runtime object for VssStore.

(cc @andrei-21)

@ldk-reviews-bot

ldk-reviews-bot commented May 19, 2025

Copy link
Copy Markdown

👋 Thanks for assigning @TheBlueMatt as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@tnull
tnull marked this pull request as draft May 19, 2025 15:10
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from d42f762 to 2f43096CompareMay 22, 2025 09:27
@tnulltnull changed the title Introduce Runtime object and allow to take a tokio::runtime::HandleIntroduce Runtime object allowng to detect outer runtime contextMay 22, 2025
@tnull
tnull marked this pull request as ready for review May 22, 2025 09:29
Comment threadsrc/chain/electrum.rs Outdated
Comment threadsrc/runtime.rs Outdated
Comment threadsrc/runtime.rs Outdated
Comment threadsrc/chain/electrum.rs Outdated
Comment threadsrc/chain/electrum.rs Outdated
Comment threadsrc/runtime.rs Outdated
Comment threadsrc/runtime.rs Outdated

@andrei-21andrei-21 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have tested with my prototype, everything works ok.

Comment threadsrc/runtime.rs Outdated
Comment threadsrc/runtime.rs Outdated

pub fn block_on<F: Future>(&self, future: F) -> Result<F::Output, RuntimeError> {
let handle = self.handle()?;
Ok(tokio::task::block_in_place(move || handle.block_on(future)))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After our offline chat, I was looking up the code for block_in_place: https://github.com/tokio-rs/tokio/blob/17d8c2b29d94550f504d8fd76d8d8aaf66095864/tokio/src/runtime/scheduler/multi_thread/worker.rs#L358

It indeed uses a thread local context inside.

If you use this anyway, isn't the only correct way to always also use tokio::runtime::Handle::try_current() ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We spoke about it, but seeing this again it feels a bit undefined to mix things in this non-transparent way.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mentioned the case where there is no current runtime though. But that is detectable too, and in that case the call doesn't need to be wrapped in block_on?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code comments exactly explaining why this block_in_place -> block_on chaining is needed would be helpful too.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, please take a look at the approach I just pushed: now went with preferring the Handle/spawned runtime everywhere, except in block_on where the outer context will take precedence.

Comment threadsrc/runtime.rs Outdated
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from 2f43096 to 2f32e15CompareMay 23, 2025 11:42
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Now pushed a new approach that initializes the Runtime in the builder, which allows to clean up a lot of the error cases. Kinda makes sense to go this way, as starting/stopping the runtime is out of our control anyways, if the user gives us a handle to use.

@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch 3 times, most recently from 3d7c8a6 to 48a42f1CompareMay 23, 2025 12:00

@joostjagerjoostjager left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice clean up of the error cases indeed.

Comment threadsrc/runtime.rs
Comment threadsrc/runtime.rs Outdated
// during `block_on`, as this is the context `block_in_place` would operate on. So we try
// to detect the outer context here, and otherwise use whatever was set during
// initialization.
let handle = tokio::runtime::Handle::try_current().unwrap_or(self.handle());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is used here, shouldn't it be used everywhere (spawn, spawn_blocking)?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm very confused: below you argue against invisible capture, here you say we should invisibly capture the context from any entry point? No, I think generally using what was set on startup and only making an exception for block_on makes sense.

@joostjagerjoostjagerMay 23, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not the same. In new the handle is saved and there's the implicit requirement for it to stay alive.

Here it isn't saved. It's just used if present, and otherwise we fall back to what the user configured and knows they need to keep alive.

Why make an exception for block_on? I'd think it is better to be consistent, and if it can't be avoid in block_on it should be done everywhere?

Comment threadsrc/runtime.rs
impl Runtime {
pub fn new() -> Result<Self, std::io::Error> {
let mode = match tokio::runtime::Handle::try_current() {
Ok(handle) => RuntimeMode::Handle(handle),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It still makes me feel a bit uncomfortable that that handle is capture here invisibly, and that there's the implicit assumption that the user will keep this alive.

Removing it here, and letting the user pass it in themselves via the builder seems to be a more transparent way to signal that it is used and needs to remain available.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I see your concern, but I'm not sure. I think the current behavior is the expected default behavior that just makes it work transparently for any upstream users. And we need to auto-detect for block_on at the very least anyways. Not sure if @TheBlueMatt has an opinion here, since he (as a user) requested auto-detection in #491?

Comment threadsrc/builder.rs
log_error!(logger, "Failed to setup tokio runtime: {}", e);
BuildError::RuntimeSetupFailed
})?)
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code reuse opportunity

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NACK, IMO it's much more readable to keep short blocks like this inlined, instead of having the reader jump all over the file, losing context.

@joostjagerjoostjagerMay 23, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't agree. I think eliminating the risk of future code changes not being applied in both places is more important than having the reader jump to a function. The function can have a descriptive name too such as build_runtime. I don't think it is bad for readability at all.

Your argument of jumping all over the file would also apply to code that isn't necessarily duplicated. Because also in that case, the reader would have to jump. I think that would lead to long function bodies, of which there are already too many in ldk, and those absolutely do not help with readability.

Comment threadsrc/event.rs
Comment threadsrc/gossip.rs
Comment threadsrc/gossip.rs
}
}

impl FutureSpawner for RuntimeSpawner {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be implemented directly onto the new Runtime?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good thought, unfortunately no as GossipVerifier::new takes FutureSpawner by value, and of course we can't impl FutureSpawner for Arc<Runtime> as both sides of it would include non-local types.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there really no way to do this? Implement an interface on a local type and then pass it to the other crate? Or would it require a different type on the LDK side for FutureSpawner?

Perhaps longer term moving Runtime to rust-lightning could be a direction too?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there really no way to do this? Implement an interface on a local type and then pass it to the other crate?

There is a way to do this, which is the newtype pattern, which is essentially what we have.

@tnulltnullMay 23, 2025

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or would it require a different type on the LDK side for FutureSpawner?

The easiest way to avoid this would be to have GossipVerifier::new take a Deref<Target = FutureSpawner> or Borrow<FutureSpawner>, but honestly a newtype isn't too bad in this case, IMO.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. I might, for the async kv store pr, use Deref<Target=FS> then already as a preparation. But ofc the wrapper is no big deal.

Comment threadsrc/lib.rs
@tnull
tnull requested a review from andrei-21May 23, 2025 12:34
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

I have tested with my prototype, everything works ok.

@andrei-21 Thanks! Mind reconfirming this works as expected with the new approach?

@tnulltnull left a comment

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, seems switching VssStore over to use the same runtime has the integration test hang. Will need some more debugging.

Comment threadsrc/lib.rs
pub fn disconnect(&self, counterparty_node_id: PublicKey) -> Result<(), Error> {
let rt_lock = self.runtime.read().unwrap();
if rt_lock.is_none() {
if !*self.is_running.read().unwrap() {

@joostjagerjoostjagerMay 23, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of scope for this PR, but curious: we talked about the type-state pattern for Runtime and that it may not be ideal. Could the pattern work for Node though? So start returning a type that exposes the method, and that type being consumed by stop.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we had considered that previously, too. Maybe, although I'm not sure if we want to force this pattern on our users, i.e., they'd need to create something akin to an enum wrapper that could hold variants StoppedNode/StartedNode, or would pipe through different versions of the node in their app, depending on the node state. Also, in the future we'd like to handle (re-)starting on persistence failure for the users, and if they hold a StartedNode object, there is really no way to force them to drop it.

@joostjagerjoostjagerMay 26, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I have no experience with the pattern. It looks pretty powerful in combination with Rust's type system. Don't know if the pipe through can be done with a wrapper around the state types. Restart could be a method on the StartedNode perhaps. Either way, this was just a side remark.

@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch 3 times, most recently from f65ad68 to 838c1bcCompareMay 23, 2025 13:17
@andrei-21

Copy link
Copy Markdown
Contributor

I have tested with my prototype, everything works ok.

@andrei-21 Thanks! Mind reconfirming this works as expected with the new approach?

Tried again, seems to work as expected.

@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Tried again, seems to work as expected.

Thanks again!

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @andrei-21! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch 8 times, most recently from 15f300b to 47207f5CompareMay 27, 2025 12:10
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from 47207f5 to b838720CompareJuly 7, 2025 07:43
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from b838720 to 636c357CompareAugust 14, 2025 13:26
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Rebased this on current main to make some progress finally. I'm tempted to punt on the VSS part of it, as we're about to get a 'real' async KVStore with #462, so dropped that commit for now.

@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from 636c357 to 56e7977CompareAugust 14, 2025 13:50
@tnull
tnull requested a review from TheBlueMattAugust 14, 2025 13:50
@tnulltnull self-assigned this Aug 14, 2025
@tnulltnull added the weekly goal Someone wants to land this this week label Aug 14, 2025
@tnulltnull moved this to Goal: Merge in Weekly GoalsAug 14, 2025
Instead of holding an `Arc<RwLock<Option<Arc<tokio::runtime::Runtime>>>`
and dealing with stuff like `tokio::task::block_in_place` at all
callsites, we introduce a `Runtime` object that takes care of the state
transitions, and allows to detect and reuse an outer runtime context.
We also adjust the `with_runtime` API to take a `tokio::runtime::Handle`
rather than an `Arc<Runtime>`.
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from 56e7977 to 4879002CompareAugust 15, 2025 13:38
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Rebased on main to resolve conflicts.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @TheBlueMatt! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diff itself looks correct. I didn't try to verify that there aren't missing places where we should check for is_running, but did leave some suggestions to improve test coverage marginally.

Comment threadsrc/lib.rs
self.logger,
"Active runtime tasks left prior to shutdown: {}",
metrics_runtime.metrics().active_tasks_count()
runtime_handle.metrics().active_tasks_count()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a debug assertion that its zero? It seems like it would be pretty easy to spawn a long-running task that loops forever, has a reference to the owned runtime, and prevents it from ever droping.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might not necessarily be 0, especially when reusing an outer runtime. Currently we still have some gossip verification and HTLC-forwarding tasks that aren't necessarily finished before this point, but the latter will def. go away with #462.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right but we really need to test that we don't have forever-running tasks which leak. As written it would be really easy for a bug to slip in that lets a task run forever and stop() leaves a handful of threads lying around. Maybe that means tests need to wait for gossip verification and HTLC forwarding tasks even if prod builds don't.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right but we really need to test that we don't have forever-running tasks which leak. As written it would be really easy for a bug to slip in that lets a task run forever and stop() leaves a handful of threads lying around.

Yes, that is a possibility. I guess we could move the newly-introduced background_tasks and cancellable_background_tasksJoinSets into Runtime, and then only expose spawn_background_task/spawn_cancellable_background_task methods, ensuring that whenever we spawn we track the JoinHandle and abort or await it on shutdown.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmm, that sounds like a much better approach than just trying to test it.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did it in #619

Comment threadsrc/payment/bolt11.rs
payment_store: Arc<PaymentStore>,
peer_store: Arc<PeerStore<Arc<Logger>>>,
config: Arc<Config>,
is_running: Arc<RwLock<bool>>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we have our own Runtime type, can we move this into the Runtime and then add a debug_assertion when spawning a new task that running is true? Seems like that would add some additional test coverage and simplify the diff somewhat.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See below: is_running is really a property of Node that (as of this PR) doesn't mean "Runtime is available" (which we now assume), but is also a reentrancy guard for runtime state transitions.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the Runtime (not tokio Runtime but rather the ldk-node Runtime) is also a "property of the Node", so why shouldn't "you can use the runtime" be in that. Again this really feels like it's missing a lot of debug assertions - bugs slipping in in the future here seems really likely.

Comment threadsrc/builder.rs
let background_tasks = Mutex::new(None);
let cancellable_background_tasks = Mutex::new(None);

let is_running = Arc::new(RwLock::new(false));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should really be an AtomicBool, not a RwLock<bool>.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we chose an RwLock for a reason, namely that we acquire and hold the lock during start/stop which keeps us from running into weird intermediate states if users would call start/stop in quick succession.

@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Landing this for now, might open a PR with minor follow-ups.

@tnull
tnull merged commit 110ab06 into lightningdevkit:mainAug 18, 2025
11 of 15 checks passed
@github-project-automationgithub-project-automationBot moved this from Goal: Merge to Done in Weekly GoalsAug 18, 2025
tnull added a commit to tnull/ldk-node that referenced this pull request Nov 3, 2025
We previously attempted to drop the internal runtime from `VssStore`,
resulting into blocking behavior. While we recently made changes that
improved our situation (having VSS CI pass again pretty reliably), we
just ran into yet another case where the VSS CI hung (cf.
https://github.com/lightningdevkit/vss-server/actions/runs/19023212819/job/54322173817?pr=59).
Here we attempt to restore even more of the original pre-
ab3d78d / lightningdevkit#543 behavior to get rid of
the reappearing blocking behavior, i.e., only use the internal runtime
in `VssStore`.
tnull added a commit to tnull/ldk-node that referenced this pull request Nov 3, 2025
We previously attempted to drop the internal runtime from `VssStore`,
resulting into blocking behavior. While we recently made changes that
improved our situation (having VSS CI pass again pretty reliably), we
just ran into yet another case where the VSS CI hung (cf.
https://github.com/lightningdevkit/vss-server/actions/runs/19023212819/job/54322173817?pr=59).
Here we attempt to restore even more of the original pre-
ab3d78d / lightningdevkit#543 behavior to get rid of
the reappearing blocking behavior, i.e., only use the internal runtime
in `VssStore`.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

weekly goalSomeone wants to land this this week

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Auto-detect existing tokio runtime?

5 participants

@tnull@ldk-reviews-bot@andrei-21@TheBlueMatt@joostjager
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Introduce `Runtime` object allowng to detect outer runtime context by tnull · Pull Request #543 · lightningdevkit/ldk-node · GitHub
Skip to content

Introduce Runtime object allowng to detect outer runtime context - #543

Merged
tnull merged 1 commit into
lightningdevkit:mainfrom
tnull:2025-05-allow-to-use-runtime-handle
Aug 18, 2025
Merged

Introduce Runtime object allowng to detect outer runtime context#543
tnull merged 1 commit into
lightningdevkit:mainfrom
tnull:2025-05-allow-to-use-runtime-handle

Conversation

@tnull

@tnulltnull commented May 19, 2025

Copy link
Copy Markdown
Collaborator

Closes#491

Instead of holding an Arc<RwLock<Option<Arc<tokio::runtime::Runtime>>>
and dealing with stuff like tokio::task::block_in_place at all
callsites, we introduce a Runtime object that takes care of the state
transitions, and allows to detect and reuse an outer runtime context.

We also adjust the with_runtime API to take a tokio::runtime::Handle
rather than an Arc<Runtime>.

We then also go ahead an reuse said Runtime object for VssStore.

(cc @andrei-21)

@ldk-reviews-bot

ldk-reviews-bot commented May 19, 2025

Copy link
Copy Markdown

👋 Thanks for assigning @TheBlueMatt as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@tnull
tnull marked this pull request as draft May 19, 2025 15:10
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from d42f762 to 2f43096CompareMay 22, 2025 09:27
@tnulltnull changed the title Introduce Runtime object and allow to take a tokio::runtime::HandleIntroduce Runtime object allowng to detect outer runtime contextMay 22, 2025
@tnull
tnull marked this pull request as ready for review May 22, 2025 09:29
Comment threadsrc/chain/electrum.rs Outdated
Comment threadsrc/runtime.rs Outdated
Comment threadsrc/runtime.rs Outdated
Comment threadsrc/chain/electrum.rs Outdated
Comment threadsrc/chain/electrum.rs Outdated
Comment threadsrc/runtime.rs Outdated
Comment threadsrc/runtime.rs Outdated

@andrei-21andrei-21 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have tested with my prototype, everything works ok.

Comment threadsrc/runtime.rs Outdated
Comment threadsrc/runtime.rs Outdated

pub fn block_on<F: Future>(&self, future: F) -> Result<F::Output, RuntimeError> {
let handle = self.handle()?;
Ok(tokio::task::block_in_place(move || handle.block_on(future)))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After our offline chat, I was looking up the code for block_in_place: https://github.com/tokio-rs/tokio/blob/17d8c2b29d94550f504d8fd76d8d8aaf66095864/tokio/src/runtime/scheduler/multi_thread/worker.rs#L358

It indeed uses a thread local context inside.

If you use this anyway, isn't the only correct way to always also use tokio::runtime::Handle::try_current() ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We spoke about it, but seeing this again it feels a bit undefined to mix things in this non-transparent way.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mentioned the case where there is no current runtime though. But that is detectable too, and in that case the call doesn't need to be wrapped in block_on?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code comments exactly explaining why this block_in_place -> block_on chaining is needed would be helpful too.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, please take a look at the approach I just pushed: now went with preferring the Handle/spawned runtime everywhere, except in block_on where the outer context will take precedence.

Comment threadsrc/runtime.rs Outdated
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from 2f43096 to 2f32e15CompareMay 23, 2025 11:42
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Now pushed a new approach that initializes the Runtime in the builder, which allows to clean up a lot of the error cases. Kinda makes sense to go this way, as starting/stopping the runtime is out of our control anyways, if the user gives us a handle to use.

@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch 3 times, most recently from 3d7c8a6 to 48a42f1CompareMay 23, 2025 12:00

@joostjagerjoostjager left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice clean up of the error cases indeed.

Comment threadsrc/runtime.rs
Comment threadsrc/runtime.rs Outdated
// during `block_on`, as this is the context `block_in_place` would operate on. So we try
// to detect the outer context here, and otherwise use whatever was set during
// initialization.
let handle = tokio::runtime::Handle::try_current().unwrap_or(self.handle());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is used here, shouldn't it be used everywhere (spawn, spawn_blocking)?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm very confused: below you argue against invisible capture, here you say we should invisibly capture the context from any entry point? No, I think generally using what was set on startup and only making an exception for block_on makes sense.

@joostjagerjoostjagerMay 23, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not the same. In new the handle is saved and there's the implicit requirement for it to stay alive.

Here it isn't saved. It's just used if present, and otherwise we fall back to what the user configured and knows they need to keep alive.

Why make an exception for block_on? I'd think it is better to be consistent, and if it can't be avoid in block_on it should be done everywhere?

Comment threadsrc/runtime.rs
impl Runtime {
pub fn new() -> Result<Self, std::io::Error> {
let mode = match tokio::runtime::Handle::try_current() {
Ok(handle) => RuntimeMode::Handle(handle),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It still makes me feel a bit uncomfortable that that handle is capture here invisibly, and that there's the implicit assumption that the user will keep this alive.

Removing it here, and letting the user pass it in themselves via the builder seems to be a more transparent way to signal that it is used and needs to remain available.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I see your concern, but I'm not sure. I think the current behavior is the expected default behavior that just makes it work transparently for any upstream users. And we need to auto-detect for block_on at the very least anyways. Not sure if @TheBlueMatt has an opinion here, since he (as a user) requested auto-detection in #491?

Comment threadsrc/builder.rs
log_error!(logger, "Failed to setup tokio runtime: {}", e);
BuildError::RuntimeSetupFailed
})?)
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code reuse opportunity

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NACK, IMO it's much more readable to keep short blocks like this inlined, instead of having the reader jump all over the file, losing context.

@joostjagerjoostjagerMay 23, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't agree. I think eliminating the risk of future code changes not being applied in both places is more important than having the reader jump to a function. The function can have a descriptive name too such as build_runtime. I don't think it is bad for readability at all.

Your argument of jumping all over the file would also apply to code that isn't necessarily duplicated. Because also in that case, the reader would have to jump. I think that would lead to long function bodies, of which there are already too many in ldk, and those absolutely do not help with readability.

Comment threadsrc/event.rs
Comment threadsrc/gossip.rs
Comment threadsrc/gossip.rs
}
}

impl FutureSpawner for RuntimeSpawner {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be implemented directly onto the new Runtime?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good thought, unfortunately no as GossipVerifier::new takes FutureSpawner by value, and of course we can't impl FutureSpawner for Arc<Runtime> as both sides of it would include non-local types.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there really no way to do this? Implement an interface on a local type and then pass it to the other crate? Or would it require a different type on the LDK side for FutureSpawner?

Perhaps longer term moving Runtime to rust-lightning could be a direction too?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there really no way to do this? Implement an interface on a local type and then pass it to the other crate?

There is a way to do this, which is the newtype pattern, which is essentially what we have.

@tnulltnullMay 23, 2025

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or would it require a different type on the LDK side for FutureSpawner?

The easiest way to avoid this would be to have GossipVerifier::new take a Deref<Target = FutureSpawner> or Borrow<FutureSpawner>, but honestly a newtype isn't too bad in this case, IMO.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. I might, for the async kv store pr, use Deref<Target=FS> then already as a preparation. But ofc the wrapper is no big deal.

Comment threadsrc/lib.rs
@tnull
tnull requested a review from andrei-21May 23, 2025 12:34
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

I have tested with my prototype, everything works ok.

@andrei-21 Thanks! Mind reconfirming this works as expected with the new approach?

@tnulltnull left a comment

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, seems switching VssStore over to use the same runtime has the integration test hang. Will need some more debugging.

Comment threadsrc/lib.rs
pub fn disconnect(&self, counterparty_node_id: PublicKey) -> Result<(), Error> {
let rt_lock = self.runtime.read().unwrap();
if rt_lock.is_none() {
if !*self.is_running.read().unwrap() {

@joostjagerjoostjagerMay 23, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of scope for this PR, but curious: we talked about the type-state pattern for Runtime and that it may not be ideal. Could the pattern work for Node though? So start returning a type that exposes the method, and that type being consumed by stop.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we had considered that previously, too. Maybe, although I'm not sure if we want to force this pattern on our users, i.e., they'd need to create something akin to an enum wrapper that could hold variants StoppedNode/StartedNode, or would pipe through different versions of the node in their app, depending on the node state. Also, in the future we'd like to handle (re-)starting on persistence failure for the users, and if they hold a StartedNode object, there is really no way to force them to drop it.

@joostjagerjoostjagerMay 26, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I have no experience with the pattern. It looks pretty powerful in combination with Rust's type system. Don't know if the pipe through can be done with a wrapper around the state types. Restart could be a method on the StartedNode perhaps. Either way, this was just a side remark.

@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch 3 times, most recently from f65ad68 to 838c1bcCompareMay 23, 2025 13:17
@andrei-21

Copy link
Copy Markdown
Contributor

I have tested with my prototype, everything works ok.

@andrei-21 Thanks! Mind reconfirming this works as expected with the new approach?

Tried again, seems to work as expected.

@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Tried again, seems to work as expected.

Thanks again!

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @andrei-21! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch 8 times, most recently from 15f300b to 47207f5CompareMay 27, 2025 12:10
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from 47207f5 to b838720CompareJuly 7, 2025 07:43
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from b838720 to 636c357CompareAugust 14, 2025 13:26
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Rebased this on current main to make some progress finally. I'm tempted to punt on the VSS part of it, as we're about to get a 'real' async KVStore with #462, so dropped that commit for now.

@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from 636c357 to 56e7977CompareAugust 14, 2025 13:50
@tnull
tnull requested a review from TheBlueMattAugust 14, 2025 13:50
@tnulltnull self-assigned this Aug 14, 2025
@tnulltnull added the weekly goal Someone wants to land this this week label Aug 14, 2025
@tnulltnull moved this to Goal: Merge in Weekly GoalsAug 14, 2025
Instead of holding an `Arc<RwLock<Option<Arc<tokio::runtime::Runtime>>>`
and dealing with stuff like `tokio::task::block_in_place` at all
callsites, we introduce a `Runtime` object that takes care of the state
transitions, and allows to detect and reuse an outer runtime context.
We also adjust the `with_runtime` API to take a `tokio::runtime::Handle`
rather than an `Arc<Runtime>`.
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from 56e7977 to 4879002CompareAugust 15, 2025 13:38
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Rebased on main to resolve conflicts.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @TheBlueMatt! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diff itself looks correct. I didn't try to verify that there aren't missing places where we should check for is_running, but did leave some suggestions to improve test coverage marginally.

Comment threadsrc/lib.rs
self.logger,
"Active runtime tasks left prior to shutdown: {}",
metrics_runtime.metrics().active_tasks_count()
runtime_handle.metrics().active_tasks_count()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a debug assertion that its zero? It seems like it would be pretty easy to spawn a long-running task that loops forever, has a reference to the owned runtime, and prevents it from ever droping.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might not necessarily be 0, especially when reusing an outer runtime. Currently we still have some gossip verification and HTLC-forwarding tasks that aren't necessarily finished before this point, but the latter will def. go away with #462.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right but we really need to test that we don't have forever-running tasks which leak. As written it would be really easy for a bug to slip in that lets a task run forever and stop() leaves a handful of threads lying around. Maybe that means tests need to wait for gossip verification and HTLC forwarding tasks even if prod builds don't.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right but we really need to test that we don't have forever-running tasks which leak. As written it would be really easy for a bug to slip in that lets a task run forever and stop() leaves a handful of threads lying around.

Yes, that is a possibility. I guess we could move the newly-introduced background_tasks and cancellable_background_tasksJoinSets into Runtime, and then only expose spawn_background_task/spawn_cancellable_background_task methods, ensuring that whenever we spawn we track the JoinHandle and abort or await it on shutdown.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmm, that sounds like a much better approach than just trying to test it.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did it in #619

Comment threadsrc/payment/bolt11.rs
payment_store: Arc<PaymentStore>,
peer_store: Arc<PeerStore<Arc<Logger>>>,
config: Arc<Config>,
is_running: Arc<RwLock<bool>>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we have our own Runtime type, can we move this into the Runtime and then add a debug_assertion when spawning a new task that running is true? Seems like that would add some additional test coverage and simplify the diff somewhat.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See below: is_running is really a property of Node that (as of this PR) doesn't mean "Runtime is available" (which we now assume), but is also a reentrancy guard for runtime state transitions.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the Runtime (not tokio Runtime but rather the ldk-node Runtime) is also a "property of the Node", so why shouldn't "you can use the runtime" be in that. Again this really feels like it's missing a lot of debug assertions - bugs slipping in in the future here seems really likely.

Comment threadsrc/builder.rs
let background_tasks = Mutex::new(None);
let cancellable_background_tasks = Mutex::new(None);

let is_running = Arc::new(RwLock::new(false));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should really be an AtomicBool, not a RwLock<bool>.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we chose an RwLock for a reason, namely that we acquire and hold the lock during start/stop which keeps us from running into weird intermediate states if users would call start/stop in quick succession.

@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Landing this for now, might open a PR with minor follow-ups.

@tnull
tnull merged commit 110ab06 into lightningdevkit:mainAug 18, 2025
11 of 15 checks passed
@github-project-automationgithub-project-automationBot moved this from Goal: Merge to Done in Weekly GoalsAug 18, 2025
tnull added a commit to tnull/ldk-node that referenced this pull request Nov 3, 2025
We previously attempted to drop the internal runtime from `VssStore`,
resulting into blocking behavior. While we recently made changes that
improved our situation (having VSS CI pass again pretty reliably), we
just ran into yet another case where the VSS CI hung (cf.
https://github.com/lightningdevkit/vss-server/actions/runs/19023212819/job/54322173817?pr=59).
Here we attempt to restore even more of the original pre-
ab3d78d / lightningdevkit#543 behavior to get rid of
the reappearing blocking behavior, i.e., only use the internal runtime
in `VssStore`.
tnull added a commit to tnull/ldk-node that referenced this pull request Nov 3, 2025
We previously attempted to drop the internal runtime from `VssStore`,
resulting into blocking behavior. While we recently made changes that
improved our situation (having VSS CI pass again pretty reliably), we
just ran into yet another case where the VSS CI hung (cf.
https://github.com/lightningdevkit/vss-server/actions/runs/19023212819/job/54322173817?pr=59).
Here we attempt to restore even more of the original pre-
ab3d78d / lightningdevkit#543 behavior to get rid of
the reappearing blocking behavior, i.e., only use the internal runtime
in `VssStore`.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

weekly goalSomeone wants to land this this week

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Auto-detect existing tokio runtime?

5 participants

@tnull@ldk-reviews-bot@andrei-21@TheBlueMatt@joostjager
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Introduce `Runtime` object allowng to detect outer runtime context by tnull · Pull Request #543 · lightningdevkit/ldk-node · GitHub
Skip to content

Introduce Runtime object allowng to detect outer runtime context - #543

Merged
tnull merged 1 commit into
lightningdevkit:mainfrom
tnull:2025-05-allow-to-use-runtime-handle
Aug 18, 2025
Merged

Introduce Runtime object allowng to detect outer runtime context#543
tnull merged 1 commit into
lightningdevkit:mainfrom
tnull:2025-05-allow-to-use-runtime-handle

Conversation

@tnull

@tnulltnull commented May 19, 2025

Copy link
Copy Markdown
Collaborator

Closes#491

Instead of holding an Arc<RwLock<Option<Arc<tokio::runtime::Runtime>>>
and dealing with stuff like tokio::task::block_in_place at all
callsites, we introduce a Runtime object that takes care of the state
transitions, and allows to detect and reuse an outer runtime context.

We also adjust the with_runtime API to take a tokio::runtime::Handle
rather than an Arc<Runtime>.

We then also go ahead an reuse said Runtime object for VssStore.

(cc @andrei-21)

@ldk-reviews-bot

ldk-reviews-bot commented May 19, 2025

Copy link
Copy Markdown

👋 Thanks for assigning @TheBlueMatt as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@tnull
tnull marked this pull request as draft May 19, 2025 15:10
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from d42f762 to 2f43096CompareMay 22, 2025 09:27
@tnulltnull changed the title Introduce Runtime object and allow to take a tokio::runtime::HandleIntroduce Runtime object allowng to detect outer runtime contextMay 22, 2025
@tnull
tnull marked this pull request as ready for review May 22, 2025 09:29
Comment threadsrc/chain/electrum.rs Outdated
Comment threadsrc/runtime.rs Outdated
Comment threadsrc/runtime.rs Outdated
Comment threadsrc/chain/electrum.rs Outdated
Comment threadsrc/chain/electrum.rs Outdated
Comment threadsrc/runtime.rs Outdated
Comment threadsrc/runtime.rs Outdated

@andrei-21andrei-21 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have tested with my prototype, everything works ok.

Comment threadsrc/runtime.rs Outdated
Comment threadsrc/runtime.rs Outdated

pub fn block_on<F: Future>(&self, future: F) -> Result<F::Output, RuntimeError> {
let handle = self.handle()?;
Ok(tokio::task::block_in_place(move || handle.block_on(future)))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After our offline chat, I was looking up the code for block_in_place: https://github.com/tokio-rs/tokio/blob/17d8c2b29d94550f504d8fd76d8d8aaf66095864/tokio/src/runtime/scheduler/multi_thread/worker.rs#L358

It indeed uses a thread local context inside.

If you use this anyway, isn't the only correct way to always also use tokio::runtime::Handle::try_current() ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We spoke about it, but seeing this again it feels a bit undefined to mix things in this non-transparent way.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mentioned the case where there is no current runtime though. But that is detectable too, and in that case the call doesn't need to be wrapped in block_on?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code comments exactly explaining why this block_in_place -> block_on chaining is needed would be helpful too.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, please take a look at the approach I just pushed: now went with preferring the Handle/spawned runtime everywhere, except in block_on where the outer context will take precedence.

Comment threadsrc/runtime.rs Outdated
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from 2f43096 to 2f32e15CompareMay 23, 2025 11:42
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Now pushed a new approach that initializes the Runtime in the builder, which allows to clean up a lot of the error cases. Kinda makes sense to go this way, as starting/stopping the runtime is out of our control anyways, if the user gives us a handle to use.

@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch 3 times, most recently from 3d7c8a6 to 48a42f1CompareMay 23, 2025 12:00

@joostjagerjoostjager left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice clean up of the error cases indeed.

Comment threadsrc/runtime.rs
Comment threadsrc/runtime.rs Outdated
// during `block_on`, as this is the context `block_in_place` would operate on. So we try
// to detect the outer context here, and otherwise use whatever was set during
// initialization.
let handle = tokio::runtime::Handle::try_current().unwrap_or(self.handle());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is used here, shouldn't it be used everywhere (spawn, spawn_blocking)?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm very confused: below you argue against invisible capture, here you say we should invisibly capture the context from any entry point? No, I think generally using what was set on startup and only making an exception for block_on makes sense.

@joostjagerjoostjagerMay 23, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not the same. In new the handle is saved and there's the implicit requirement for it to stay alive.

Here it isn't saved. It's just used if present, and otherwise we fall back to what the user configured and knows they need to keep alive.

Why make an exception for block_on? I'd think it is better to be consistent, and if it can't be avoid in block_on it should be done everywhere?

Comment threadsrc/runtime.rs
impl Runtime {
pub fn new() -> Result<Self, std::io::Error> {
let mode = match tokio::runtime::Handle::try_current() {
Ok(handle) => RuntimeMode::Handle(handle),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It still makes me feel a bit uncomfortable that that handle is capture here invisibly, and that there's the implicit assumption that the user will keep this alive.

Removing it here, and letting the user pass it in themselves via the builder seems to be a more transparent way to signal that it is used and needs to remain available.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I see your concern, but I'm not sure. I think the current behavior is the expected default behavior that just makes it work transparently for any upstream users. And we need to auto-detect for block_on at the very least anyways. Not sure if @TheBlueMatt has an opinion here, since he (as a user) requested auto-detection in #491?

Comment threadsrc/builder.rs
log_error!(logger, "Failed to setup tokio runtime: {}", e);
BuildError::RuntimeSetupFailed
})?)
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code reuse opportunity

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NACK, IMO it's much more readable to keep short blocks like this inlined, instead of having the reader jump all over the file, losing context.

@joostjagerjoostjagerMay 23, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't agree. I think eliminating the risk of future code changes not being applied in both places is more important than having the reader jump to a function. The function can have a descriptive name too such as build_runtime. I don't think it is bad for readability at all.

Your argument of jumping all over the file would also apply to code that isn't necessarily duplicated. Because also in that case, the reader would have to jump. I think that would lead to long function bodies, of which there are already too many in ldk, and those absolutely do not help with readability.

Comment threadsrc/event.rs
Comment threadsrc/gossip.rs
Comment threadsrc/gossip.rs
}
}

impl FutureSpawner for RuntimeSpawner {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be implemented directly onto the new Runtime?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good thought, unfortunately no as GossipVerifier::new takes FutureSpawner by value, and of course we can't impl FutureSpawner for Arc<Runtime> as both sides of it would include non-local types.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there really no way to do this? Implement an interface on a local type and then pass it to the other crate? Or would it require a different type on the LDK side for FutureSpawner?

Perhaps longer term moving Runtime to rust-lightning could be a direction too?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there really no way to do this? Implement an interface on a local type and then pass it to the other crate?

There is a way to do this, which is the newtype pattern, which is essentially what we have.

@tnulltnullMay 23, 2025

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or would it require a different type on the LDK side for FutureSpawner?

The easiest way to avoid this would be to have GossipVerifier::new take a Deref<Target = FutureSpawner> or Borrow<FutureSpawner>, but honestly a newtype isn't too bad in this case, IMO.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. I might, for the async kv store pr, use Deref<Target=FS> then already as a preparation. But ofc the wrapper is no big deal.

Comment threadsrc/lib.rs
@tnull
tnull requested a review from andrei-21May 23, 2025 12:34
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

I have tested with my prototype, everything works ok.

@andrei-21 Thanks! Mind reconfirming this works as expected with the new approach?

@tnulltnull left a comment

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, seems switching VssStore over to use the same runtime has the integration test hang. Will need some more debugging.

Comment threadsrc/lib.rs
pub fn disconnect(&self, counterparty_node_id: PublicKey) -> Result<(), Error> {
let rt_lock = self.runtime.read().unwrap();
if rt_lock.is_none() {
if !*self.is_running.read().unwrap() {

@joostjagerjoostjagerMay 23, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of scope for this PR, but curious: we talked about the type-state pattern for Runtime and that it may not be ideal. Could the pattern work for Node though? So start returning a type that exposes the method, and that type being consumed by stop.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we had considered that previously, too. Maybe, although I'm not sure if we want to force this pattern on our users, i.e., they'd need to create something akin to an enum wrapper that could hold variants StoppedNode/StartedNode, or would pipe through different versions of the node in their app, depending on the node state. Also, in the future we'd like to handle (re-)starting on persistence failure for the users, and if they hold a StartedNode object, there is really no way to force them to drop it.

@joostjagerjoostjagerMay 26, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I have no experience with the pattern. It looks pretty powerful in combination with Rust's type system. Don't know if the pipe through can be done with a wrapper around the state types. Restart could be a method on the StartedNode perhaps. Either way, this was just a side remark.

@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch 3 times, most recently from f65ad68 to 838c1bcCompareMay 23, 2025 13:17
@andrei-21

Copy link
Copy Markdown
Contributor

I have tested with my prototype, everything works ok.

@andrei-21 Thanks! Mind reconfirming this works as expected with the new approach?

Tried again, seems to work as expected.

@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Tried again, seems to work as expected.

Thanks again!

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @andrei-21! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch 8 times, most recently from 15f300b to 47207f5CompareMay 27, 2025 12:10
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from 47207f5 to b838720CompareJuly 7, 2025 07:43
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from b838720 to 636c357CompareAugust 14, 2025 13:26
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Rebased this on current main to make some progress finally. I'm tempted to punt on the VSS part of it, as we're about to get a 'real' async KVStore with #462, so dropped that commit for now.

@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from 636c357 to 56e7977CompareAugust 14, 2025 13:50
@tnull
tnull requested a review from TheBlueMattAugust 14, 2025 13:50
@tnulltnull self-assigned this Aug 14, 2025
@tnulltnull added the weekly goal Someone wants to land this this week label Aug 14, 2025
@tnulltnull moved this to Goal: Merge in Weekly GoalsAug 14, 2025
Instead of holding an `Arc<RwLock<Option<Arc<tokio::runtime::Runtime>>>`
and dealing with stuff like `tokio::task::block_in_place` at all
callsites, we introduce a `Runtime` object that takes care of the state
transitions, and allows to detect and reuse an outer runtime context.
We also adjust the `with_runtime` API to take a `tokio::runtime::Handle`
rather than an `Arc<Runtime>`.
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from 56e7977 to 4879002CompareAugust 15, 2025 13:38
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Rebased on main to resolve conflicts.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @TheBlueMatt! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diff itself looks correct. I didn't try to verify that there aren't missing places where we should check for is_running, but did leave some suggestions to improve test coverage marginally.

Comment threadsrc/lib.rs
self.logger,
"Active runtime tasks left prior to shutdown: {}",
metrics_runtime.metrics().active_tasks_count()
runtime_handle.metrics().active_tasks_count()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a debug assertion that its zero? It seems like it would be pretty easy to spawn a long-running task that loops forever, has a reference to the owned runtime, and prevents it from ever droping.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might not necessarily be 0, especially when reusing an outer runtime. Currently we still have some gossip verification and HTLC-forwarding tasks that aren't necessarily finished before this point, but the latter will def. go away with #462.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right but we really need to test that we don't have forever-running tasks which leak. As written it would be really easy for a bug to slip in that lets a task run forever and stop() leaves a handful of threads lying around. Maybe that means tests need to wait for gossip verification and HTLC forwarding tasks even if prod builds don't.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right but we really need to test that we don't have forever-running tasks which leak. As written it would be really easy for a bug to slip in that lets a task run forever and stop() leaves a handful of threads lying around.

Yes, that is a possibility. I guess we could move the newly-introduced background_tasks and cancellable_background_tasksJoinSets into Runtime, and then only expose spawn_background_task/spawn_cancellable_background_task methods, ensuring that whenever we spawn we track the JoinHandle and abort or await it on shutdown.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmm, that sounds like a much better approach than just trying to test it.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did it in #619

Comment threadsrc/payment/bolt11.rs
payment_store: Arc<PaymentStore>,
peer_store: Arc<PeerStore<Arc<Logger>>>,
config: Arc<Config>,
is_running: Arc<RwLock<bool>>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we have our own Runtime type, can we move this into the Runtime and then add a debug_assertion when spawning a new task that running is true? Seems like that would add some additional test coverage and simplify the diff somewhat.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See below: is_running is really a property of Node that (as of this PR) doesn't mean "Runtime is available" (which we now assume), but is also a reentrancy guard for runtime state transitions.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the Runtime (not tokio Runtime but rather the ldk-node Runtime) is also a "property of the Node", so why shouldn't "you can use the runtime" be in that. Again this really feels like it's missing a lot of debug assertions - bugs slipping in in the future here seems really likely.

Comment threadsrc/builder.rs
let background_tasks = Mutex::new(None);
let cancellable_background_tasks = Mutex::new(None);

let is_running = Arc::new(RwLock::new(false));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should really be an AtomicBool, not a RwLock<bool>.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we chose an RwLock for a reason, namely that we acquire and hold the lock during start/stop which keeps us from running into weird intermediate states if users would call start/stop in quick succession.

@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Landing this for now, might open a PR with minor follow-ups.

@tnull
tnull merged commit 110ab06 into lightningdevkit:mainAug 18, 2025
11 of 15 checks passed
@github-project-automationgithub-project-automationBot moved this from Goal: Merge to Done in Weekly GoalsAug 18, 2025
tnull added a commit to tnull/ldk-node that referenced this pull request Nov 3, 2025
We previously attempted to drop the internal runtime from `VssStore`,
resulting into blocking behavior. While we recently made changes that
improved our situation (having VSS CI pass again pretty reliably), we
just ran into yet another case where the VSS CI hung (cf.
https://github.com/lightningdevkit/vss-server/actions/runs/19023212819/job/54322173817?pr=59).
Here we attempt to restore even more of the original pre-
ab3d78d / lightningdevkit#543 behavior to get rid of
the reappearing blocking behavior, i.e., only use the internal runtime
in `VssStore`.
tnull added a commit to tnull/ldk-node that referenced this pull request Nov 3, 2025
We previously attempted to drop the internal runtime from `VssStore`,
resulting into blocking behavior. While we recently made changes that
improved our situation (having VSS CI pass again pretty reliably), we
just ran into yet another case where the VSS CI hung (cf.
https://github.com/lightningdevkit/vss-server/actions/runs/19023212819/job/54322173817?pr=59).
Here we attempt to restore even more of the original pre-
ab3d78d / lightningdevkit#543 behavior to get rid of
the reappearing blocking behavior, i.e., only use the internal runtime
in `VssStore`.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

weekly goalSomeone wants to land this this week

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Auto-detect existing tokio runtime?

5 participants

@tnull@ldk-reviews-bot@andrei-21@TheBlueMatt@joostjager
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Introduce `Runtime` object allowng to detect outer runtime context by tnull · Pull Request #543 · lightningdevkit/ldk-node · GitHub
Skip to content

Introduce Runtime object allowng to detect outer runtime context - #543

Merged
tnull merged 1 commit into
lightningdevkit:mainfrom
tnull:2025-05-allow-to-use-runtime-handle
Aug 18, 2025
Merged

Introduce Runtime object allowng to detect outer runtime context#543
tnull merged 1 commit into
lightningdevkit:mainfrom
tnull:2025-05-allow-to-use-runtime-handle

Conversation

@tnull

@tnulltnull commented May 19, 2025

Copy link
Copy Markdown
Collaborator

Closes#491

Instead of holding an Arc<RwLock<Option<Arc<tokio::runtime::Runtime>>>
and dealing with stuff like tokio::task::block_in_place at all
callsites, we introduce a Runtime object that takes care of the state
transitions, and allows to detect and reuse an outer runtime context.

We also adjust the with_runtime API to take a tokio::runtime::Handle
rather than an Arc<Runtime>.

We then also go ahead an reuse said Runtime object for VssStore.

(cc @andrei-21)

@ldk-reviews-bot

ldk-reviews-bot commented May 19, 2025

Copy link
Copy Markdown

👋 Thanks for assigning @TheBlueMatt as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@tnull
tnull marked this pull request as draft May 19, 2025 15:10
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from d42f762 to 2f43096CompareMay 22, 2025 09:27
@tnulltnull changed the title Introduce Runtime object and allow to take a tokio::runtime::HandleIntroduce Runtime object allowng to detect outer runtime contextMay 22, 2025
@tnull
tnull marked this pull request as ready for review May 22, 2025 09:29
Comment threadsrc/chain/electrum.rs Outdated
Comment threadsrc/runtime.rs Outdated
Comment threadsrc/runtime.rs Outdated
Comment threadsrc/chain/electrum.rs Outdated
Comment threadsrc/chain/electrum.rs Outdated
Comment threadsrc/runtime.rs Outdated
Comment threadsrc/runtime.rs Outdated

@andrei-21andrei-21 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have tested with my prototype, everything works ok.

Comment threadsrc/runtime.rs Outdated
Comment threadsrc/runtime.rs Outdated

pub fn block_on<F: Future>(&self, future: F) -> Result<F::Output, RuntimeError> {
let handle = self.handle()?;
Ok(tokio::task::block_in_place(move || handle.block_on(future)))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After our offline chat, I was looking up the code for block_in_place: https://github.com/tokio-rs/tokio/blob/17d8c2b29d94550f504d8fd76d8d8aaf66095864/tokio/src/runtime/scheduler/multi_thread/worker.rs#L358

It indeed uses a thread local context inside.

If you use this anyway, isn't the only correct way to always also use tokio::runtime::Handle::try_current() ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We spoke about it, but seeing this again it feels a bit undefined to mix things in this non-transparent way.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mentioned the case where there is no current runtime though. But that is detectable too, and in that case the call doesn't need to be wrapped in block_on?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code comments exactly explaining why this block_in_place -> block_on chaining is needed would be helpful too.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, please take a look at the approach I just pushed: now went with preferring the Handle/spawned runtime everywhere, except in block_on where the outer context will take precedence.

Comment threadsrc/runtime.rs Outdated
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from 2f43096 to 2f32e15CompareMay 23, 2025 11:42
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Now pushed a new approach that initializes the Runtime in the builder, which allows to clean up a lot of the error cases. Kinda makes sense to go this way, as starting/stopping the runtime is out of our control anyways, if the user gives us a handle to use.

@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch 3 times, most recently from 3d7c8a6 to 48a42f1CompareMay 23, 2025 12:00

@joostjagerjoostjager left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice clean up of the error cases indeed.

Comment threadsrc/runtime.rs
Comment threadsrc/runtime.rs Outdated
// during `block_on`, as this is the context `block_in_place` would operate on. So we try
// to detect the outer context here, and otherwise use whatever was set during
// initialization.
let handle = tokio::runtime::Handle::try_current().unwrap_or(self.handle());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is used here, shouldn't it be used everywhere (spawn, spawn_blocking)?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm very confused: below you argue against invisible capture, here you say we should invisibly capture the context from any entry point? No, I think generally using what was set on startup and only making an exception for block_on makes sense.

@joostjagerjoostjagerMay 23, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not the same. In new the handle is saved and there's the implicit requirement for it to stay alive.

Here it isn't saved. It's just used if present, and otherwise we fall back to what the user configured and knows they need to keep alive.

Why make an exception for block_on? I'd think it is better to be consistent, and if it can't be avoid in block_on it should be done everywhere?

Comment threadsrc/runtime.rs
impl Runtime {
pub fn new() -> Result<Self, std::io::Error> {
let mode = match tokio::runtime::Handle::try_current() {
Ok(handle) => RuntimeMode::Handle(handle),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It still makes me feel a bit uncomfortable that that handle is capture here invisibly, and that there's the implicit assumption that the user will keep this alive.

Removing it here, and letting the user pass it in themselves via the builder seems to be a more transparent way to signal that it is used and needs to remain available.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I see your concern, but I'm not sure. I think the current behavior is the expected default behavior that just makes it work transparently for any upstream users. And we need to auto-detect for block_on at the very least anyways. Not sure if @TheBlueMatt has an opinion here, since he (as a user) requested auto-detection in #491?

Comment threadsrc/builder.rs
log_error!(logger, "Failed to setup tokio runtime: {}", e);
BuildError::RuntimeSetupFailed
})?)
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code reuse opportunity

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NACK, IMO it's much more readable to keep short blocks like this inlined, instead of having the reader jump all over the file, losing context.

@joostjagerjoostjagerMay 23, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't agree. I think eliminating the risk of future code changes not being applied in both places is more important than having the reader jump to a function. The function can have a descriptive name too such as build_runtime. I don't think it is bad for readability at all.

Your argument of jumping all over the file would also apply to code that isn't necessarily duplicated. Because also in that case, the reader would have to jump. I think that would lead to long function bodies, of which there are already too many in ldk, and those absolutely do not help with readability.

Comment threadsrc/event.rs
Comment threadsrc/gossip.rs
Comment threadsrc/gossip.rs
}
}

impl FutureSpawner for RuntimeSpawner {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be implemented directly onto the new Runtime?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good thought, unfortunately no as GossipVerifier::new takes FutureSpawner by value, and of course we can't impl FutureSpawner for Arc<Runtime> as both sides of it would include non-local types.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there really no way to do this? Implement an interface on a local type and then pass it to the other crate? Or would it require a different type on the LDK side for FutureSpawner?

Perhaps longer term moving Runtime to rust-lightning could be a direction too?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there really no way to do this? Implement an interface on a local type and then pass it to the other crate?

There is a way to do this, which is the newtype pattern, which is essentially what we have.

@tnulltnullMay 23, 2025

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or would it require a different type on the LDK side for FutureSpawner?

The easiest way to avoid this would be to have GossipVerifier::new take a Deref<Target = FutureSpawner> or Borrow<FutureSpawner>, but honestly a newtype isn't too bad in this case, IMO.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. I might, for the async kv store pr, use Deref<Target=FS> then already as a preparation. But ofc the wrapper is no big deal.

Comment threadsrc/lib.rs
@tnull
tnull requested a review from andrei-21May 23, 2025 12:34
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

I have tested with my prototype, everything works ok.

@andrei-21 Thanks! Mind reconfirming this works as expected with the new approach?

@tnulltnull left a comment

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, seems switching VssStore over to use the same runtime has the integration test hang. Will need some more debugging.

Comment threadsrc/lib.rs
pub fn disconnect(&self, counterparty_node_id: PublicKey) -> Result<(), Error> {
let rt_lock = self.runtime.read().unwrap();
if rt_lock.is_none() {
if !*self.is_running.read().unwrap() {

@joostjagerjoostjagerMay 23, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of scope for this PR, but curious: we talked about the type-state pattern for Runtime and that it may not be ideal. Could the pattern work for Node though? So start returning a type that exposes the method, and that type being consumed by stop.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we had considered that previously, too. Maybe, although I'm not sure if we want to force this pattern on our users, i.e., they'd need to create something akin to an enum wrapper that could hold variants StoppedNode/StartedNode, or would pipe through different versions of the node in their app, depending on the node state. Also, in the future we'd like to handle (re-)starting on persistence failure for the users, and if they hold a StartedNode object, there is really no way to force them to drop it.

@joostjagerjoostjagerMay 26, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I have no experience with the pattern. It looks pretty powerful in combination with Rust's type system. Don't know if the pipe through can be done with a wrapper around the state types. Restart could be a method on the StartedNode perhaps. Either way, this was just a side remark.

@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch 3 times, most recently from f65ad68 to 838c1bcCompareMay 23, 2025 13:17
@andrei-21

Copy link
Copy Markdown
Contributor

I have tested with my prototype, everything works ok.

@andrei-21 Thanks! Mind reconfirming this works as expected with the new approach?

Tried again, seems to work as expected.

@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Tried again, seems to work as expected.

Thanks again!

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @andrei-21! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch 8 times, most recently from 15f300b to 47207f5CompareMay 27, 2025 12:10
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from 47207f5 to b838720CompareJuly 7, 2025 07:43
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from b838720 to 636c357CompareAugust 14, 2025 13:26
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Rebased this on current main to make some progress finally. I'm tempted to punt on the VSS part of it, as we're about to get a 'real' async KVStore with #462, so dropped that commit for now.

@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from 636c357 to 56e7977CompareAugust 14, 2025 13:50
@tnull
tnull requested a review from TheBlueMattAugust 14, 2025 13:50
@tnulltnull self-assigned this Aug 14, 2025
@tnulltnull added the weekly goal Someone wants to land this this week label Aug 14, 2025
@tnulltnull moved this to Goal: Merge in Weekly GoalsAug 14, 2025
Instead of holding an `Arc<RwLock<Option<Arc<tokio::runtime::Runtime>>>`
and dealing with stuff like `tokio::task::block_in_place` at all
callsites, we introduce a `Runtime` object that takes care of the state
transitions, and allows to detect and reuse an outer runtime context.
We also adjust the `with_runtime` API to take a `tokio::runtime::Handle`
rather than an `Arc<Runtime>`.
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from 56e7977 to 4879002CompareAugust 15, 2025 13:38
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Rebased on main to resolve conflicts.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @TheBlueMatt! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diff itself looks correct. I didn't try to verify that there aren't missing places where we should check for is_running, but did leave some suggestions to improve test coverage marginally.

Comment threadsrc/lib.rs
self.logger,
"Active runtime tasks left prior to shutdown: {}",
metrics_runtime.metrics().active_tasks_count()
runtime_handle.metrics().active_tasks_count()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a debug assertion that its zero? It seems like it would be pretty easy to spawn a long-running task that loops forever, has a reference to the owned runtime, and prevents it from ever droping.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might not necessarily be 0, especially when reusing an outer runtime. Currently we still have some gossip verification and HTLC-forwarding tasks that aren't necessarily finished before this point, but the latter will def. go away with #462.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right but we really need to test that we don't have forever-running tasks which leak. As written it would be really easy for a bug to slip in that lets a task run forever and stop() leaves a handful of threads lying around. Maybe that means tests need to wait for gossip verification and HTLC forwarding tasks even if prod builds don't.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right but we really need to test that we don't have forever-running tasks which leak. As written it would be really easy for a bug to slip in that lets a task run forever and stop() leaves a handful of threads lying around.

Yes, that is a possibility. I guess we could move the newly-introduced background_tasks and cancellable_background_tasksJoinSets into Runtime, and then only expose spawn_background_task/spawn_cancellable_background_task methods, ensuring that whenever we spawn we track the JoinHandle and abort or await it on shutdown.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmm, that sounds like a much better approach than just trying to test it.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did it in #619

Comment threadsrc/payment/bolt11.rs
payment_store: Arc<PaymentStore>,
peer_store: Arc<PeerStore<Arc<Logger>>>,
config: Arc<Config>,
is_running: Arc<RwLock<bool>>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we have our own Runtime type, can we move this into the Runtime and then add a debug_assertion when spawning a new task that running is true? Seems like that would add some additional test coverage and simplify the diff somewhat.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See below: is_running is really a property of Node that (as of this PR) doesn't mean "Runtime is available" (which we now assume), but is also a reentrancy guard for runtime state transitions.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the Runtime (not tokio Runtime but rather the ldk-node Runtime) is also a "property of the Node", so why shouldn't "you can use the runtime" be in that. Again this really feels like it's missing a lot of debug assertions - bugs slipping in in the future here seems really likely.

Comment threadsrc/builder.rs
let background_tasks = Mutex::new(None);
let cancellable_background_tasks = Mutex::new(None);

let is_running = Arc::new(RwLock::new(false));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should really be an AtomicBool, not a RwLock<bool>.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we chose an RwLock for a reason, namely that we acquire and hold the lock during start/stop which keeps us from running into weird intermediate states if users would call start/stop in quick succession.

@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Landing this for now, might open a PR with minor follow-ups.

@tnull
tnull merged commit 110ab06 into lightningdevkit:mainAug 18, 2025
11 of 15 checks passed
@github-project-automationgithub-project-automationBot moved this from Goal: Merge to Done in Weekly GoalsAug 18, 2025
tnull added a commit to tnull/ldk-node that referenced this pull request Nov 3, 2025
We previously attempted to drop the internal runtime from `VssStore`,
resulting into blocking behavior. While we recently made changes that
improved our situation (having VSS CI pass again pretty reliably), we
just ran into yet another case where the VSS CI hung (cf.
https://github.com/lightningdevkit/vss-server/actions/runs/19023212819/job/54322173817?pr=59).
Here we attempt to restore even more of the original pre-
ab3d78d / lightningdevkit#543 behavior to get rid of
the reappearing blocking behavior, i.e., only use the internal runtime
in `VssStore`.
tnull added a commit to tnull/ldk-node that referenced this pull request Nov 3, 2025
We previously attempted to drop the internal runtime from `VssStore`,
resulting into blocking behavior. While we recently made changes that
improved our situation (having VSS CI pass again pretty reliably), we
just ran into yet another case where the VSS CI hung (cf.
https://github.com/lightningdevkit/vss-server/actions/runs/19023212819/job/54322173817?pr=59).
Here we attempt to restore even more of the original pre-
ab3d78d / lightningdevkit#543 behavior to get rid of
the reappearing blocking behavior, i.e., only use the internal runtime
in `VssStore`.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

weekly goalSomeone wants to land this this week

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Auto-detect existing tokio runtime?

5 participants

@tnull@ldk-reviews-bot@andrei-21@TheBlueMatt@joostjager
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Introduce `Runtime` object allowng to detect outer runtime context by tnull · Pull Request #543 · lightningdevkit/ldk-node · GitHub
Skip to content

Introduce Runtime object allowng to detect outer runtime context - #543

Merged
tnull merged 1 commit into
lightningdevkit:mainfrom
tnull:2025-05-allow-to-use-runtime-handle
Aug 18, 2025
Merged

Introduce Runtime object allowng to detect outer runtime context#543
tnull merged 1 commit into
lightningdevkit:mainfrom
tnull:2025-05-allow-to-use-runtime-handle

Conversation

@tnull

@tnulltnull commented May 19, 2025

Copy link
Copy Markdown
Collaborator

Closes#491

Instead of holding an Arc<RwLock<Option<Arc<tokio::runtime::Runtime>>>
and dealing with stuff like tokio::task::block_in_place at all
callsites, we introduce a Runtime object that takes care of the state
transitions, and allows to detect and reuse an outer runtime context.

We also adjust the with_runtime API to take a tokio::runtime::Handle
rather than an Arc<Runtime>.

We then also go ahead an reuse said Runtime object for VssStore.

(cc @andrei-21)

@ldk-reviews-bot

ldk-reviews-bot commented May 19, 2025

Copy link
Copy Markdown

👋 Thanks for assigning @TheBlueMatt as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@tnull
tnull marked this pull request as draft May 19, 2025 15:10
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from d42f762 to 2f43096CompareMay 22, 2025 09:27
@tnulltnull changed the title Introduce Runtime object and allow to take a tokio::runtime::HandleIntroduce Runtime object allowng to detect outer runtime contextMay 22, 2025
@tnull
tnull marked this pull request as ready for review May 22, 2025 09:29
Comment threadsrc/chain/electrum.rs Outdated
Comment threadsrc/runtime.rs Outdated
Comment threadsrc/runtime.rs Outdated
Comment threadsrc/chain/electrum.rs Outdated
Comment threadsrc/chain/electrum.rs Outdated
Comment threadsrc/runtime.rs Outdated
Comment threadsrc/runtime.rs Outdated

@andrei-21andrei-21 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have tested with my prototype, everything works ok.

Comment threadsrc/runtime.rs Outdated
Comment threadsrc/runtime.rs Outdated

pub fn block_on<F: Future>(&self, future: F) -> Result<F::Output, RuntimeError> {
let handle = self.handle()?;
Ok(tokio::task::block_in_place(move || handle.block_on(future)))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After our offline chat, I was looking up the code for block_in_place: https://github.com/tokio-rs/tokio/blob/17d8c2b29d94550f504d8fd76d8d8aaf66095864/tokio/src/runtime/scheduler/multi_thread/worker.rs#L358

It indeed uses a thread local context inside.

If you use this anyway, isn't the only correct way to always also use tokio::runtime::Handle::try_current() ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We spoke about it, but seeing this again it feels a bit undefined to mix things in this non-transparent way.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mentioned the case where there is no current runtime though. But that is detectable too, and in that case the call doesn't need to be wrapped in block_on?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code comments exactly explaining why this block_in_place -> block_on chaining is needed would be helpful too.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, please take a look at the approach I just pushed: now went with preferring the Handle/spawned runtime everywhere, except in block_on where the outer context will take precedence.

Comment threadsrc/runtime.rs Outdated
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from 2f43096 to 2f32e15CompareMay 23, 2025 11:42
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Now pushed a new approach that initializes the Runtime in the builder, which allows to clean up a lot of the error cases. Kinda makes sense to go this way, as starting/stopping the runtime is out of our control anyways, if the user gives us a handle to use.

@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch 3 times, most recently from 3d7c8a6 to 48a42f1CompareMay 23, 2025 12:00

@joostjagerjoostjager left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice clean up of the error cases indeed.

Comment threadsrc/runtime.rs
Comment threadsrc/runtime.rs Outdated
// during `block_on`, as this is the context `block_in_place` would operate on. So we try
// to detect the outer context here, and otherwise use whatever was set during
// initialization.
let handle = tokio::runtime::Handle::try_current().unwrap_or(self.handle());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is used here, shouldn't it be used everywhere (spawn, spawn_blocking)?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm very confused: below you argue against invisible capture, here you say we should invisibly capture the context from any entry point? No, I think generally using what was set on startup and only making an exception for block_on makes sense.

@joostjagerjoostjagerMay 23, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not the same. In new the handle is saved and there's the implicit requirement for it to stay alive.

Here it isn't saved. It's just used if present, and otherwise we fall back to what the user configured and knows they need to keep alive.

Why make an exception for block_on? I'd think it is better to be consistent, and if it can't be avoid in block_on it should be done everywhere?

Comment threadsrc/runtime.rs
impl Runtime {
pub fn new() -> Result<Self, std::io::Error> {
let mode = match tokio::runtime::Handle::try_current() {
Ok(handle) => RuntimeMode::Handle(handle),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It still makes me feel a bit uncomfortable that that handle is capture here invisibly, and that there's the implicit assumption that the user will keep this alive.

Removing it here, and letting the user pass it in themselves via the builder seems to be a more transparent way to signal that it is used and needs to remain available.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I see your concern, but I'm not sure. I think the current behavior is the expected default behavior that just makes it work transparently for any upstream users. And we need to auto-detect for block_on at the very least anyways. Not sure if @TheBlueMatt has an opinion here, since he (as a user) requested auto-detection in #491?

Comment threadsrc/builder.rs
log_error!(logger, "Failed to setup tokio runtime: {}", e);
BuildError::RuntimeSetupFailed
})?)
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code reuse opportunity

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NACK, IMO it's much more readable to keep short blocks like this inlined, instead of having the reader jump all over the file, losing context.

@joostjagerjoostjagerMay 23, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't agree. I think eliminating the risk of future code changes not being applied in both places is more important than having the reader jump to a function. The function can have a descriptive name too such as build_runtime. I don't think it is bad for readability at all.

Your argument of jumping all over the file would also apply to code that isn't necessarily duplicated. Because also in that case, the reader would have to jump. I think that would lead to long function bodies, of which there are already too many in ldk, and those absolutely do not help with readability.

Comment threadsrc/event.rs
Comment threadsrc/gossip.rs
Comment threadsrc/gossip.rs
}
}

impl FutureSpawner for RuntimeSpawner {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be implemented directly onto the new Runtime?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good thought, unfortunately no as GossipVerifier::new takes FutureSpawner by value, and of course we can't impl FutureSpawner for Arc<Runtime> as both sides of it would include non-local types.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there really no way to do this? Implement an interface on a local type and then pass it to the other crate? Or would it require a different type on the LDK side for FutureSpawner?

Perhaps longer term moving Runtime to rust-lightning could be a direction too?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there really no way to do this? Implement an interface on a local type and then pass it to the other crate?

There is a way to do this, which is the newtype pattern, which is essentially what we have.

@tnulltnullMay 23, 2025

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or would it require a different type on the LDK side for FutureSpawner?

The easiest way to avoid this would be to have GossipVerifier::new take a Deref<Target = FutureSpawner> or Borrow<FutureSpawner>, but honestly a newtype isn't too bad in this case, IMO.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. I might, for the async kv store pr, use Deref<Target=FS> then already as a preparation. But ofc the wrapper is no big deal.

Comment threadsrc/lib.rs
@tnull
tnull requested a review from andrei-21May 23, 2025 12:34
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

I have tested with my prototype, everything works ok.

@andrei-21 Thanks! Mind reconfirming this works as expected with the new approach?

@tnulltnull left a comment

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, seems switching VssStore over to use the same runtime has the integration test hang. Will need some more debugging.

Comment threadsrc/lib.rs
pub fn disconnect(&self, counterparty_node_id: PublicKey) -> Result<(), Error> {
let rt_lock = self.runtime.read().unwrap();
if rt_lock.is_none() {
if !*self.is_running.read().unwrap() {

@joostjagerjoostjagerMay 23, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of scope for this PR, but curious: we talked about the type-state pattern for Runtime and that it may not be ideal. Could the pattern work for Node though? So start returning a type that exposes the method, and that type being consumed by stop.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we had considered that previously, too. Maybe, although I'm not sure if we want to force this pattern on our users, i.e., they'd need to create something akin to an enum wrapper that could hold variants StoppedNode/StartedNode, or would pipe through different versions of the node in their app, depending on the node state. Also, in the future we'd like to handle (re-)starting on persistence failure for the users, and if they hold a StartedNode object, there is really no way to force them to drop it.

@joostjagerjoostjagerMay 26, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I have no experience with the pattern. It looks pretty powerful in combination with Rust's type system. Don't know if the pipe through can be done with a wrapper around the state types. Restart could be a method on the StartedNode perhaps. Either way, this was just a side remark.

@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch 3 times, most recently from f65ad68 to 838c1bcCompareMay 23, 2025 13:17
@andrei-21

Copy link
Copy Markdown
Contributor

I have tested with my prototype, everything works ok.

@andrei-21 Thanks! Mind reconfirming this works as expected with the new approach?

Tried again, seems to work as expected.

@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Tried again, seems to work as expected.

Thanks again!

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @andrei-21! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch 8 times, most recently from 15f300b to 47207f5CompareMay 27, 2025 12:10
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from 47207f5 to b838720CompareJuly 7, 2025 07:43
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from b838720 to 636c357CompareAugust 14, 2025 13:26
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Rebased this on current main to make some progress finally. I'm tempted to punt on the VSS part of it, as we're about to get a 'real' async KVStore with #462, so dropped that commit for now.

@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from 636c357 to 56e7977CompareAugust 14, 2025 13:50
@tnull
tnull requested a review from TheBlueMattAugust 14, 2025 13:50
@tnulltnull self-assigned this Aug 14, 2025
@tnulltnull added the weekly goal Someone wants to land this this week label Aug 14, 2025
@tnulltnull moved this to Goal: Merge in Weekly GoalsAug 14, 2025
Instead of holding an `Arc<RwLock<Option<Arc<tokio::runtime::Runtime>>>`
and dealing with stuff like `tokio::task::block_in_place` at all
callsites, we introduce a `Runtime` object that takes care of the state
transitions, and allows to detect and reuse an outer runtime context.
We also adjust the `with_runtime` API to take a `tokio::runtime::Handle`
rather than an `Arc<Runtime>`.
@tnull
tnullforce-pushed the 2025-05-allow-to-use-runtime-handle branch from 56e7977 to 4879002CompareAugust 15, 2025 13:38
@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Rebased on main to resolve conflicts.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @TheBlueMatt! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diff itself looks correct. I didn't try to verify that there aren't missing places where we should check for is_running, but did leave some suggestions to improve test coverage marginally.

Comment threadsrc/lib.rs
self.logger,
"Active runtime tasks left prior to shutdown: {}",
metrics_runtime.metrics().active_tasks_count()
runtime_handle.metrics().active_tasks_count()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a debug assertion that its zero? It seems like it would be pretty easy to spawn a long-running task that loops forever, has a reference to the owned runtime, and prevents it from ever droping.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might not necessarily be 0, especially when reusing an outer runtime. Currently we still have some gossip verification and HTLC-forwarding tasks that aren't necessarily finished before this point, but the latter will def. go away with #462.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right but we really need to test that we don't have forever-running tasks which leak. As written it would be really easy for a bug to slip in that lets a task run forever and stop() leaves a handful of threads lying around. Maybe that means tests need to wait for gossip verification and HTLC forwarding tasks even if prod builds don't.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right but we really need to test that we don't have forever-running tasks which leak. As written it would be really easy for a bug to slip in that lets a task run forever and stop() leaves a handful of threads lying around.

Yes, that is a possibility. I guess we could move the newly-introduced background_tasks and cancellable_background_tasksJoinSets into Runtime, and then only expose spawn_background_task/spawn_cancellable_background_task methods, ensuring that whenever we spawn we track the JoinHandle and abort or await it on shutdown.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmm, that sounds like a much better approach than just trying to test it.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did it in #619

Comment threadsrc/payment/bolt11.rs
payment_store: Arc<PaymentStore>,
peer_store: Arc<PeerStore<Arc<Logger>>>,
config: Arc<Config>,
is_running: Arc<RwLock<bool>>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we have our own Runtime type, can we move this into the Runtime and then add a debug_assertion when spawning a new task that running is true? Seems like that would add some additional test coverage and simplify the diff somewhat.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See below: is_running is really a property of Node that (as of this PR) doesn't mean "Runtime is available" (which we now assume), but is also a reentrancy guard for runtime state transitions.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the Runtime (not tokio Runtime but rather the ldk-node Runtime) is also a "property of the Node", so why shouldn't "you can use the runtime" be in that. Again this really feels like it's missing a lot of debug assertions - bugs slipping in in the future here seems really likely.

Comment threadsrc/builder.rs
let background_tasks = Mutex::new(None);
let cancellable_background_tasks = Mutex::new(None);

let is_running = Arc::new(RwLock::new(false));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should really be an AtomicBool, not a RwLock<bool>.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we chose an RwLock for a reason, namely that we acquire and hold the lock during start/stop which keeps us from running into weird intermediate states if users would call start/stop in quick succession.

@tnull

Copy link
Copy Markdown
CollaboratorAuthor

Landing this for now, might open a PR with minor follow-ups.

@tnull
tnull merged commit 110ab06 into lightningdevkit:mainAug 18, 2025
11 of 15 checks passed
@github-project-automationgithub-project-automationBot moved this from Goal: Merge to Done in Weekly GoalsAug 18, 2025
tnull added a commit to tnull/ldk-node that referenced this pull request Nov 3, 2025
We previously attempted to drop the internal runtime from `VssStore`,
resulting into blocking behavior. While we recently made changes that
improved our situation (having VSS CI pass again pretty reliably), we
just ran into yet another case where the VSS CI hung (cf.
https://github.com/lightningdevkit/vss-server/actions/runs/19023212819/job/54322173817?pr=59).
Here we attempt to restore even more of the original pre-
ab3d78d / lightningdevkit#543 behavior to get rid of
the reappearing blocking behavior, i.e., only use the internal runtime
in `VssStore`.
tnull added a commit to tnull/ldk-node that referenced this pull request Nov 3, 2025
We previously attempted to drop the internal runtime from `VssStore`,
resulting into blocking behavior. While we recently made changes that
improved our situation (having VSS CI pass again pretty reliably), we
just ran into yet another case where the VSS CI hung (cf.
https://github.com/lightningdevkit/vss-server/actions/runs/19023212819/job/54322173817?pr=59).
Here we attempt to restore even more of the original pre-
ab3d78d / lightningdevkit#543 behavior to get rid of
the reappearing blocking behavior, i.e., only use the internal runtime
in `VssStore`.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

weekly goalSomeone wants to land this this week

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Auto-detect existing tokio runtime?

5 participants

@tnull@ldk-reviews-bot@andrei-21@TheBlueMatt@joostjager