-
Notifications
You must be signed in to change notification settings - Fork 76
Async persistence #1235
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Async persistence #1235
Conversation
Pull Request Test Coverage Report for Build 20388045567Details
💛 - Coveralls |
nothingmuch
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
concept ACK.
for dealing with the extension trait duplication without macros implementing the moral equivalent of keyword generics, which i agree is too complex, the various methods of the extension trait can be replaced non-pub methods of the transition result objects instead
so for example:
persister.save_maybe_fatal_or_success_transition(self).awaitwould instead be something like:
let (outcome, event) = self.deconstruct()?;
persister.save_event(event);
Ok(outcome)if the various deconstruction methods are pub(crate) or private then the abstraction boundaries are still respected, and the destructuring and control flow logic is identical for both async and non async
this would require only minimal boilerplate to be duplicated between the two "flavors" of function/trait
| fn save_event( | ||
| &self, | ||
| event: Self::SessionEvent, | ||
| ) -> impl std::future::Future<Output = Result<(), Self::InternalStorageError>> + Send; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
async methods in traits are supported since 1.75, so within our msrv, so i think this can just be an async fn
does this need trait objects for a reason i'm missing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had the same thought, but according to the linter:
error: use of `async fn` in public traits is discouraged as auto trait bounds cannot be specified
--> payjoin/src/core/persist.rs:531:5
|
531 | async fn save_event(&self, event: Self::SessionEvent)
| ^^^^^
|
= note: you can suppress this lint if you plan to use the trait only in your own code, or do not care about auto traits like `Send` on the `Future`
= note: `-D async-fn-in-trait` implied by `-D warnings`
= help: to override `-D warnings` add `#[allow(async_fn_in_trait)]`
help: you can alternatively desugar to a normal `fn` that returns `impl Future` and add any desired bounds such as `Send`, but these cannot be relaxed without a breaking API change
|
531 ~ fn save_event(&self, event: Self::SessionEvent)
532 ~ -> impl std::future::Future<Output = Result<(), Self::InternalStorageError>> + Send;
|
InternalAsyncSessionPersister is private so it can and does use the async fn syntax.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This still allows the use of async fn within impls of the trait. However, it also means that the trait will never be compatible with impls where the returned Future of the method does not implement Send.
hmm, IIUC i think that's fine for our use case since event data is value typed and that's all the persister deals with, but i'm not confident in this assement
assuming this stays desuraged, please add a comment linking to the warning's docs or at least mentioning its name
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The release notes and description of the error message are here. The desugared version seems fine, though the trait-variant macro crate option seems viable especially because it comes straight from the rust-lang org. IIRC Lexe is a multithreaded environment that would consume these async functions where the Send auto trait would be critical.
arminsabouri
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Concept ACK
I had note about the test suite (happy to hack on that part with you as I originally wrote it). Most of the meat of this PR is going to be de-dup'ing the internal logic for handling the different state transition objects -- which I imagine claude will be pretty good at.
Thanks for starting this up!
payjoin/src/core/persist.rs
Outdated
| expected_result: ExpectedResult<SuccessState, ErrorState>, | ||
| } | ||
|
|
||
| async fn do_test_async< |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need an async version of the test harness and test cases? I would think we can refactor this test suite so we can run the same tests with a sync and async persister. And the results should be equivalent.
68953e7 to
1b3304c
Compare
Conceptually, Storage errors are distinct from API errors. Storage errors are never persisted and reflect an application/implementation error. This commit splits API errors into their own enum to better reflect this distinction.
1b3304c to
2315f73
Compare
Split `save()` into distinct "deconstruction" and "execution" steps, and have the `deconstruct()` method live on the Transition structs directly. `deconstruct()` returns a `PersistAction` which tells the persister what action to take (do nothing, save an event, or save an event and close the session).
This gives the implementer the choice of to implement and call an asynchronous persister as an alternative to the existing synchronous one, by exposing the `AsyncSessionPersister` trait and `save_async` method in the API alongside their synchronous counterparts.
Simplify the `TestCase` struct by making the transition types in the struct directly. This allows more granular control over how the test cases are performed, which enables adding async tests with minimal code duplication in the next commit.
2315f73 to
54b566f
Compare
|
We discussed a bit by signal and I think adding the async API serves a real demand. Regarding the first commit, excuse my slow climb back into programming here and let me ask: is refactoring those errors a necessary part of this PR or is it mostly unrelated? |
It is becoming clear that we need to ship an async persister trait alongside the blocking one to accommodate real developer needs. We've been discussing this in #816 and most recently this need came up again regarding the LDK-node integration.
This PR introduces the
AsyncSessionPersistertrait as an async counterpart to the synchronousSessionPersister, along with thesave_asyncmethod for persisting state transition results.Leaving as draft because the current approach makes no attempt to refactor any internal logic and is rather WET, but it gives us a foundation for the new API surface and async unit tests to validate it. Claude code wrote most of this copypasta (sloppypasta?).
It seems to me the DRYest approach will involve macros (see lightning-macros for some async macro examples). I'm not super thrilled about the prospect because
persist.rsis already the most abstract part of the codebase, and while macros are cool they're not exactly easy to reason about. I'd like to get other opinions before going down a refactoring route cc @arminsabouri @nothingmuch @DanGouldEDIT: we will also need to do this for the payjoin-ffi
JsonReceiver/SenderPersisters which are not covered in this draft.Pull Request Checklist
Please confirm the following before requesting review:
AI
in the body of this PR.