') + ')', '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('^' + ".*" + ', '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" + ', '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('^' + ".*" + ', '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); } })(); })(); Implement `KVStore` for `TestStore` by tnull · Pull Request #4069 · lightningdevkit/rust-lightning · GitHub
Skip to content

Implement KVStore for TestStore - #4069

Merged
TheBlueMatt merged 2 commits into
lightningdevkit:mainfrom
tnull:2025-09-async-test-store
Sep 16, 2025
Merged

Implement KVStore for TestStore#4069
TheBlueMatt merged 2 commits into
lightningdevkit:mainfrom
tnull:2025-09-async-test-store

Conversation

@tnull

@tnulltnull commented Sep 12, 2025

Copy link
Copy Markdown
Contributor

We implement the async KVStore trait for TestStore. This is mostly useful in LDK Node tests, where we use TestStore and we now require all stores to implement both variants.

Moreover, we drop the Frankenstein-esque process_events_async_with_kv_store_sync which won't be necessary anymore for LDK Node.

@ldk-reviews-bot

ldk-reviews-bot commented Sep 12, 2025

Copy link
Copy Markdown

👋 Thanks for assigning @joostjager 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.


/// Async events processor that is based on [`process_events_async`] but allows for [`KVStoreSync`] to be used for
/// synchronous background persistence.
pub async fn process_events_async_with_kv_store_sync<

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hmm, not quite sure I understand why we want to drop this? ldk-node might not use it, but I imagine some others might? Its the equivalent of our previous async BP loop and keeping it makes upgrades easier for those who might not want to switch to partial-async-kvstore immediately?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

IMO it's a very odd middleground API that was introduced when the KVStoreSyncWrapper wasn't public. I fear users might find it confusing, and just using KVStoreSyncWrapper when needed seems way more consistent (as they'd already need to do that for some of the other types they'd hand into that method anyways).

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.

No comment. It's so little code that I think it's fine either way.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

IMO we should prefer users not use KVStoreSyncWrapper directly. The docs even explicitly say "It is not necessary to use this type directly." (and I feel like we should #[doc(hidden)] it?).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Hmm, but at least in LDK Node using KVStoreSyncWrapper was unavoidable. I can drop the drop commit if you insist, but IMO it's a pretty awkward confusing API.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Now dropped the drop commit since you prefer to keep it.

Comment threadlightning/src/util/test_utils.rs Outdated
}
}

impl KVStore for TestStore {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I wonder if we shouldn't actually make these async? eg write the returned future to require two polls or something? Maybe its not worth it but seems like we should consider it. It feels a bit weird to have the test implementation do something that no real implementation should do.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Alright, now added a fixup to do just that.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Now force-pushed a further simplification, also allowing to drop the 'splitting' commit.

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.

Oh I like this double poll thing for testing. A bit too fake otherwise.

@codecov

codecovBot commented Sep 12, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 61.29032% with 24 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.34%. Comparing base (867f084) to head (012bb7a).
⚠️ Report is 39 commits behind head on main.

Files with missing linesPatch %Lines
lightning/src/util/test_utils.rs50.00%24 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #4069 +/- ##
==========================================
- Coverage 88.39% 88.34% -0.05% 
==========================================
Files 177 177 Lines 131314 131922 +608 Branches 131314 131922 +608 ==========================================
+ Hits 116069 116550 +481 - Misses 12596 12708 +112 - Partials 2649 2664 +15 
FlagCoverage Δ
fuzzing21.60% <0.00%> (-0.41%)⬇️
tests88.18% <61.29%> (-0.05%)⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment threadlightning/src/util/test_utils.rs Outdated
let secondary_namespace = secondary_namespace.to_string();
let key = key.to_string();
let inner = Arc::clone(&self.inner);
Box::pin(async move {

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.

Before returning this, I think we need to record the order?

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.

Or spawn_blocking and await?

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.

Or just call it because it isn't actually blocking?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I don't think so, as it calls through to the inner Mutex? Ah, actually, now that we made it actually async as of #4069 (comment), we might need to?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Or just call it because it isn't actually blocking?

You mean actually async? It is now, so we might need to do the whole write-tracking thing now..

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Nevermind, I think now that we retrieve the result sync again (while acquiring the inner Mutex), we should be good ordering-wise?

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.

Yes I think it's good now

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This has regressed again - its no longer correct. You should be able to drop the second-to-last commit and just do the actual operation sync and then return a future of the result.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This has regressed again

Yes, I explicitly asked you whether to revert to this version though? So you don't want the intial version, but the intermediate version that implements the more complicated future logic, but doens't require polling twice. Okay.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This has regressed again - its no longer correct. You should be able to drop the second-to-last commit and just do the actual operation sync and then return a future of the result.

Pushed another fully-sync-under-the-hood variant. Let me know if that's the right amount of regression.

@tnull
tnullforce-pushed the 2025-09-async-test-store branch from 7faae86 to 3ef76a5CompareSeptember 12, 2025 13:34
Comment threadlightning/src/util/test_utils.rs Outdated
@tnull
tnullforce-pushed the 2025-09-async-test-store branch from 3ef76a5 to 9df68fcCompareSeptember 12, 2025 13:55
@tnull
tnullforce-pushed the 2025-09-async-test-store branch from 9df68fc to 3dc60fdCompareSeptember 12, 2025 14:30
joostjager
joostjager previously approved these changes Sep 12, 2025

@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.

I understand this is going to be used in ldk-node, but I don't think any of the async code is hit in ldk itself. It's basically dead code within that scope. Test coverage of test code is maybe a bit over the top?


/// Async events processor that is based on [`process_events_async`] but allows for [`KVStoreSync`] to be used for
/// synchronous background persistence.
pub async fn process_events_async_with_kv_store_sync<

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.

No comment. It's so little code that I think it's fine either way.

Comment threadlightning/src/util/test_utils.rs Outdated
let secondary_namespace = secondary_namespace.to_string();
let key = key.to_string();
let inner = Arc::clone(&self.inner);
Box::pin(async move {

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.

Yes I think it's good now

Comment threadlightning/src/util/test_utils.rs Outdated
}
}

impl KVStore for TestStore {

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.

Oh I like this double poll thing for testing. A bit too fake otherwise.

@TheBlueMattTheBlueMatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

There's a ton of room to make TestStore a better test, dunno if it has to happen here but if you have a minute it would be nice to make it a really good test.

Comment threadlightning/src/util/test_utils.rs Outdated
Comment threadlightning/src/util/test_utils.rs
@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.

@tnull
tnullforce-pushed the 2025-09-async-test-store branch from cd28a16 to c47ca84CompareSeptember 15, 2025 08:37
Comment threadlightning/src/util/test_utils.rs Outdated
@tnull
tnullforce-pushed the 2025-09-async-test-store branch from c47ca84 to 444f408CompareSeptember 16, 2025 05:16
@tnull

Copy link
Copy Markdown
ContributorAuthor

Now reverted back to the initial approach 444f408.

@tnulltnull changed the title Implement KVStore for TestStore, drop process_events_async_with_kv_store_syncImplement KVStore for TestStoreSep 16, 2025
@tnull

Copy link
Copy Markdown
ContributorAuthor

Seems CI error is pre-existing?

@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.

When reverting, did this also undo the changes that followed from #4069 (comment)?

I think currently the order is lost again?

@TheBlueMatt
TheBlueMatt removed their request for review September 16, 2025 12:34
.. to be easier reusable by different trait implementations.
We implement the async `KVStore` trait for `TestStore`. This is mostly
useful in LDK Node tests, where we use `TestStore` and we now require
all stores to implement both variants.
@tnull

Copy link
Copy Markdown
ContributorAuthor

Pushed another simplified fully-sync variant. Let me know if that works for you guys.

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

This is now quite trivial, just gonna land it.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@tnull@ldk-reviews-bot@TheBlueMatt@joostjager