Skip to content
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

[POC] generalized requests to support sync and async #212

Closed
wants to merge 2 commits into from

Conversation

stevenroose
Copy link
Collaborator

@stevenroose stevenroose commented Mar 1, 2022

So this is just a proof-of-concept of an idea I've had to finally have a way to support both sync and async in this crate.

It could go hand in hand with supporting batching and deprecating old-hyper in favor of the custom HTTP transport from LDK.

Basically, it introduces a Request struct that basically holds everything you need to perform an RPC and then it has some methods sync (perform the request sync), async (perform the request async) and batch (add the request to a batch).

In principle, this construction could even be pulled upstream the jsonrpc crate. Based on this construction we can decide how to change the API to support sync and async. Options are (1) use features to have the same method name have different signatures for sync/async, (2) support both and have _sync and _async method or (3) some kind of hybrid like I did in this POC.

As you see, inside the RpcApi impl, there is one actual impl method _request and the others are basically templated and could easily be made into a macro. However I think this crate previously moved away from having macros, so I'm not against just writing it all out in full.

Everything is open for discussion, the new construction compiles if you comment out all the other method not yet converted in the RpcApi trait..


fn converter_hex<T: bitcoin::consensus::encode::Decodable>(raw: &RawValue) -> Result<T> {
let hex: &str = serde_json::from_str(raw.get())?;
let bytes = Vec::<u8>::from_hex(&hex)?;
Copy link
Collaborator

Choose a reason for hiding this comment

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

once bitcoin_hashes is released we can skip the Vec allocation with rust-bitcoin/bitcoin_hashes#135

@JeremyRubin
Copy link

once concept would be to have only async API, but provide a blocking client executor that uses non-async internally.

This would keep the core API pure (and always async), but allow using the async code from non async functions with the sync client.

@RCasatta
Copy link
Collaborator

RCasatta commented Mar 2, 2022

once concept would be to have only async API, but provide a blocking client executor that uses non-async internally.

This would keep the core API pure (and always async), but allow using the async code from non async functions with the sync client.

One advantage of sync is that it requires a lot less dependency, using async+blocking client would prevent that

@JeremyRubin
Copy link

Not quite true; for example you can build a simple executor https://rust-lang.github.io/async-book/02_execution/04_executor.html and not pull in e..g tokio

pub struct Request<'c, T: 'static> {
method: &'static str,
args: Vec<serde_json::Value>, //TODO(stevenroose) something more efficient?
converter: &'static dyn Fn(&RawValue) -> Result<T>,
Copy link

Choose a reason for hiding this comment

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

I believe you can build this into the method signatures instead of explicitly calling a converter (e.g., https://github.com/lightningdevkit/rust-lightning/blob/5e86bbf97030c69b0118d1bef39554febfb1cbef/lightning-block-sync/src/rpc.rs#L40-L41).

Choose a reason for hiding this comment

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

👍

the only exception being if we need >1 "TryFrom" for a given type?

Copy link

Choose a reason for hiding this comment

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

Yeah, the RPC example is assuming JSON responses, which could be examined and converted as needed (e.g. https://github.com/lightningdevkit/rust-lightning/blob/b89f8815c3d40d8c5bf9aa60af5b3fbb2ee78f25/lightning-block-sync/src/convert.rs#L42-L72).

But in the following REST example, the response could be either JSON or binary depending on the parameters, which I think is what you are alluding to.

https://github.com/lightningdevkit/rust-lightning/blob/b89f8815c3d40d8c5bf9aa60af5b3fbb2ee78f25/lightning-block-sync/src/rest.rs#L29-L35

There the response format needs to be explicit at the call site. For example:

https://github.com/lightningdevkit/rust-lightning/blob/b89f8815c3d40d8c5bf9aa60af5b3fbb2ee78f25/lightning-block-sync/src/rest.rs#L38-L58

So basically, you define conversion from bytes to some response format:

https://github.com/lightningdevkit/rust-lightning/blob/b89f8815c3d40d8c5bf9aa60af5b3fbb2ee78f25/lightning-block-sync/src/http.rs#L456-L478

And then any conversions from those formats to the desired types.

conv(&raw)
}

fn async(self, client: &dyn RpcClient) -> Result<T> {
Copy link

Choose a reason for hiding this comment

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

Should this also be an async method?

let res: Response = self.call("getnetworkinfo", &[])?;
Ok(res.version)
}
#[cfg(not(feature = "async"))]
Copy link
Member

Choose a reason for hiding this comment

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

Ideally the API should support both, no? That way two dependencies setting different feature flags wouldn't cause compile failure.

Choose a reason for hiding this comment

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

spoke with @stevenroose about this, we think there is a very clean approach for this by impl future for the request type directly. he's prototyping it :)

@sander2
Copy link
Contributor

sander2 commented Mar 22, 2022

@stevenroose any update on this? We're quite interested in having async support - in the past @gregdhill made #144 but unfortunately that never went anywhere

@JeremyRubin
Copy link

in theory if you need something right away, you can use https://crates.io/crates/bitcoincore-rpc-json-async

note that this uses the sapio flavoured bitcoin crates, though, not the standard ones. I would accept a patch that let's you choose via a flag which dep you take.

@stevenroose
Copy link
Collaborator Author

Big update on this. I did a WIP/POC refactor to rust-jsonrpc and to this branch. It's not finished, I will have to add two traits that will have all the actual client's methods SyncClient and AsyncClient with the appropriate return types, but internally they will use all the methods inside the requests module.

I went kind of in a rabbit hole to reduce allocations, but the result is quite an ugly construction that requires double references &'r &'r str etc which is ugly but might also not be optimal (I have no idea about performance hits because of this, but I guess it can't be worse than allocating lots of things again?)

There's still some things I didn't figure out, like wrappers around argument inputs, but I think we might be able to create the wrappers on the trait level, so that would in the end be possible. Right now some of those arguments that need internal conversion are just re-allocated, but those could be optimized using such wrapper types lateron.

For now, I'll be working on the traits that should just be grinding through all the calls twice (I might be tempted to write a Rust macro for this, but maybe I'll just do a vim macro to manually convert them) to build the traits.

The end result should be two traits (SyncClient and AsyncClient) with all calls and then also a Batch object with all the calls but all 3 places internally should use the requests module.

@stevenroose
Copy link
Collaborator Author

This is almost finished, but blocked on rust-bitcoin/rust-bitcoin#1785 since this create bumped the rust-bitcoin dep to 0.30.

@Kixunil
Copy link

Kixunil commented Apr 9, 2023

Is there any chance of having generic HTTP implementation so that hyper can be optionally used?

Copy link

@Kixunil Kixunil left a comment

Choose a reason for hiding this comment

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

I only reviewed this lightly.

let bytes = bitcoin::consensus::encode::serialize(self);
HexParam::write_hex(&bytes[..], f)
}
}
Copy link

Choose a reason for hiding this comment

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

bitcoin 0.30 has direct support for this which is also significantly more performant. See bitcoin::consensus::serde.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah yeah I focussed on optimizing all the parsing stuff but didn't really look too much at the input stuff. This is indeed quite a silly impl.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmm, the consensus::serde stuff is interesting, I'll take a look.

into_json_from!(u64);
into_json_from!(i32);
into_json_from!(i64);
into_json_from!(f64);
Copy link

Choose a reason for hiding this comment

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

I'm not sure what these do but it looks like maybe you're trying to do what erased-serde should be used for?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmm, I don't think it's relased to the erased crate. But arguably it's just a way to limit the Into<serde_json::Value> to anything not-string.

@@ -1,6 +1,6 @@
[package]
name = "bitcoincore-rpc-json"
version = "0.16.0"
version = "1.0.0-rc0"
Copy link

Choose a reason for hiding this comment

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

This looks quite bold considering crazy-new change and the amount of breaking changes at Core.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah version is a draft. Can be anything, but I'd like to be able to release rc's while also still being able to patch the current branch. So either it would be 0.20 or something or 1.0. Doesn't have to be too bold, we can go until 2^64-1 probably with versions ;)

Copy link

Choose a reason for hiding this comment

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

1.0+ means there should be minimal if any breaking changes.

pub fr_75th: Amount,
#[serde(with = "bitcoin::util::amount::serde::as_sat")]
pub fr_90th: Amount,
#[serde(with = "serde_feerate::sat_per_vb")]
Copy link

Choose a reason for hiding this comment

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

Oh, we should add this to bitcoin.

json/src/lib.rs Outdated

/// Type to represent feerates.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FeeRate {
Copy link

Choose a reason for hiding this comment

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

We already have this in bitcoin.

@stevenroose
Copy link
Collaborator Author

Is there any chance of having generic HTTP implementation so that hyper can be optionally used?

I'm planning a compile feature to support hyper, yeah. IIRC there is already code for that in the jsonrpc crate, in this crate it would be simply having a feature-gated constructor, I guess that uses that one.

@stevenroose stevenroose force-pushed the syncasync branch 3 times, most recently from c1e7e7b to 029e778 Compare April 10, 2023 23:36
@stevenroose
Copy link
Collaborator Author

I'm going to close this and open a new one.

Sign up for free to 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.

7 participants