Create Stream for create_history_stream using async-stream #10

Merged
notplants merged 16 commits from async-stream into main 2022-01-06 15:31:57 +00:00
Owner

This PR shows a working example of changing create_history_stream to produce a Stream<Item = Result<SsbMessageValue, GolgiError>> instead of a Vec<SsbMessageValue>

Examples of how this can be used are included in examples/ssb-stream-example.rs.

Currently, two of the examples in that file are commented out, because I am running into an issue with the borrow checker, where after calling sbot_client.create_history_stream once, something is borrowed, and I cannot call it a second time. Aka the commented out examples also work, if uncommented, but you can only uncomment one at a time, currently.

I am hopeful that this is fixable without a major change to the way this PR handles streams. Similarly, I currently included get_source_stream as a method in sbot.rs instead of as a function in utils.rs next to the other similar functions, because it was easier for me to reason about borrowing working in sbot.rs, but I imagine we can also change this - just need to figure out correct way to pass rpc_reader to the function. I wanted to focus on getting the stream code working first.

One of the important commented out examples is like this:

let mut history_stream = sbot_client.create_history_stream(author.to_string()).await?;
let results : Vec<SsbMessageValue> = history_stream.try_collect().await?;
for x in results {
   println!("x: {:?}", x);
}

It shows how in one line we can convert this stream into a Vec<SsbMessageValue> using try_collect. This shows that if we change our base functions to return streams, its purely 'additive' on the previous functionality. We can easily convert into Vec and work like that if we want to, and we can keep things as Stream and work like that where it would be advantageous.

I ended up using the stream! macro from the async-stream crate, as suggested on the rust forum: https://users.rust-lang.org/t/how-to-create-async-std-stream-which-calls-async-function-in-poll-next/69760. As described in my question, I was having issues figuring out how to produce a stream while calling an async function. Maybe there are other ways, but I didn't figure it out. This was a pretty good related article https://gendignoux.com/blog/2021/04/01/rust-async-streams-futures-part1.html, but still couldn't get something matching exactly what we need from this. But its working with the macro so maybe thats fine.

So outstanding TODOS:

  • move get_source_stream to utils.rs
  • figure out how to make create_history_stream callable multiple times
  • figure out if lifetimes are really needed for create_history_stream and get_source_stream or if there is a better way to do this (might also be related to the above two points). I haven't used lifetimes much before, and am not sure if the way I am using them is necessary or correct, but I couldn't get the code working yet without them

If you want to try experiment with any of the above three points, please feel free and let me know. Or if you have other comments etc., but hopefully having this working example is a good starting point for any further improvements

This PR shows a working example of changing create_history_stream to produce a `Stream<Item = Result<SsbMessageValue, GolgiError>>` instead of a `Vec<SsbMessageValue>` Examples of how this can be used are included in `examples/ssb-stream-example.rs`. Currently, two of the examples in that file are commented out, because I am running into an issue with the borrow checker, where after calling sbot_client.create_history_stream once, something is borrowed, and I cannot call it a second time. Aka the commented out examples also work, if uncommented, but you can only uncomment one at a time, currently. I am hopeful that this is fixable without a major change to the way this PR handles streams. Similarly, I currently included get_source_stream as a method in sbot.rs instead of as a function in utils.rs next to the other similar functions, because it was easier for me to reason about borrowing working in sbot.rs, but I imagine we can also change this - just need to figure out correct way to pass rpc_reader to the function. I wanted to focus on getting the stream code working first. One of the important commented out examples is like this: ``` rust let mut history_stream = sbot_client.create_history_stream(author.to_string()).await?; let results : Vec<SsbMessageValue> = history_stream.try_collect().await?; for x in results { println!("x: {:?}", x); } ``` It shows how in one line we can convert this stream into a `Vec<SsbMessageValue>` using try_collect. This shows that if we change our base functions to return streams, its purely 'additive' on the previous functionality. We can easily convert into Vec and work like that if we want to, and we can keep things as Stream and work like that where it would be advantageous. I ended up using the stream! macro from the async-stream crate, as suggested on the rust forum: https://users.rust-lang.org/t/how-to-create-async-std-stream-which-calls-async-function-in-poll-next/69760. As described in my question, I was having issues figuring out how to produce a stream while calling an async function. Maybe there are other ways, but I didn't figure it out. This was a pretty good related article https://gendignoux.com/blog/2021/04/01/rust-async-streams-futures-part1.html, but still couldn't get something matching exactly what we need from this. But its working with the macro so maybe thats fine. So outstanding TODOS: - move get_source_stream to utils.rs - figure out how to make create_history_stream callable multiple times - figure out if lifetimes are really needed for create_history_stream and get_source_stream or if there is a better way to do this (might also be related to the above two points). I haven't used lifetimes much before, and am not sure if the way I am using them is necessary or correct, but I couldn't get the code working yet without them If you want to try experiment with any of the above three points, please feel free and let me know. Or if you have other comments etc., but hopefully having this working example is a good starting point for any further improvements
notplants added 9 commits 2022-01-04 20:17:54 +00:00
notplants requested review from glyph 2022-01-04 20:19:09 +00:00
src/messages.rs Outdated
@@ -63,3 +63,3 @@
/// Helper function which returns true if this message is of the given type,
/// and false if the type does not match or is not found
pub fn is_message_type(&self, message_type: SsbMessageContentType) -> bool {
pub fn is_message_type(&self, _message_type: SsbMessageContentType) -> bool {
Author
Owner

I think this was introduced by cargo fix, but I'm not sure why

I think this was introduced by cargo fix, but I'm not sure why
Owner

Oh, interesting. I never use cargo fix.

Oh, interesting. I never use `cargo fix`.
notplants marked this conversation as resolved
glyph left a comment
Owner

This looks great overall! It's a major plus to have the ability of returning a Vec or a Stream; super nice to have that level of flexibility at this early stage of the library development.

I've left a number of comments, including a way of moving get_source_stream into utils.

As mentioned on cabal, I believe the best way to allow multiple stream-returning sbot function calls is to simply instantiate additional connection to the sbot (reminder that sbot::init() still needs to change to sbot::connect()). The ssb-stream-example.rs can be updated to use this pattern. The examples are really nice to get others up-and-running quickly.

Another thing I saw (when running cargo nightly clippy) but didn't mention in the comments:

warning: avoid using `collect()` when not needed
   --> src/sbot.rs:187:84
    |
187 |           let messages: Vec<SsbMessageValue> = kvts.into_iter().map(|kvt| kvt.value).collect();
    |                                                                                      ^^^^^^^
...
192 |           let mut about_messages: Vec<SsbMessageValue> = messages
    |  ________________________________________________________-
193 | |             .into_iter()
    | |________________________- the iterator could be used here instead
    |
    = note: `#[warn(clippy::needless_collect)]` on by default
    = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_collect
help: use the original Iterator instead of collecting it and then producing a new one
    |
187 ~         
188 |         // TODO: after fixing sbot regression,
189 |         // change this subset query to filter by type about in addition to author
190 |         // and remove this filter section
191 |         // filter down to about  messages
192 ~         let mut about_messages: Vec<SsbMessageValue> = kvts.into_iter().map(|kvt| kvt.value)
This looks great overall! It's a major plus to have the ability of returning a `Vec` or a `Stream`; super nice to have that level of flexibility at this early stage of the library development. I've left a number of comments, including a way of moving `get_source_stream` into utils. As mentioned on `cabal`, I believe the best way to allow multiple stream-returning sbot function calls is to simply instantiate additional connection to the sbot (reminder that `sbot::init()` still needs to change to `sbot::connect()`). The `ssb-stream-example.rs` can be updated to use this pattern. The examples are really nice to get others up-and-running quickly. Another thing I saw (when running `cargo nightly clippy`) but didn't mention in the comments: ```shell warning: avoid using `collect()` when not needed --> src/sbot.rs:187:84 | 187 | let messages: Vec<SsbMessageValue> = kvts.into_iter().map(|kvt| kvt.value).collect(); | ^^^^^^^ ... 192 | let mut about_messages: Vec<SsbMessageValue> = messages | ________________________________________________________- 193 | | .into_iter() | |________________________- the iterator could be used here instead | = note: `#[warn(clippy::needless_collect)]` on by default = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_collect help: use the original Iterator instead of collecting it and then producing a new one | 187 ~ 188 | // TODO: after fixing sbot regression, 189 | // change this subset query to filter by type about in addition to author 190 | // and remove this filter section 191 | // filter down to about messages 192 ~ let mut about_messages: Vec<SsbMessageValue> = kvts.into_iter().map(|kvt| kvt.value) ```
Cargo.toml Outdated
@@ -16,2 +16,4 @@
serde = { version = "1", features = ["derive"] }
serde_json = "1"
async-stream = "0.3.2"
rand = "0.8.4"
Owner

I think this rand dependency must have slipped in during your async stream experimentation.

I think this `rand` dependency must have slipped in during your async stream experimentation.
notplants marked this conversation as resolved
@@ -31,6 +30,7 @@ pub struct SsbMessageValue {
// Enum representing the different possible message content types
#[derive(Debug)]
#[allow(missing_docs)]
Owner

I'd prefer if everything was documented, even if it seems self evident from the type definition.

This one might be something like:

/// Scuttlebutt message content `type` variants.
I'd prefer if everything was documented, even if it seems self evident from the type definition. This one might be something like: ```rust /// Scuttlebutt message content `type` variants. ```
Author
Owner

I added this about structs because according to the current lint, you don't just need the message you provided, but I believe you need a comment for each type variant, or each struct field, in addition to the overall struct.

I added this about structs because according to the current lint, you don't just need the message you provided, but I believe you need a comment for each type variant, or each struct field, in addition to the overall struct.
notplants marked this conversation as resolved
src/sbot.rs Outdated
@@ -1,5 +1,8 @@
//! Sbot type and associated methods.
use std::fmt::Debug;
use async_std::net::TcpStream;
Owner

Could rather be combined:

use async_std::{
    net::TcpStream,
    stream::{Stream, StreamExt},
};
Could rather be combined: ```rust use async_std::{ net::TcpStream, stream::{Stream, StreamExt}, }; ```
notplants marked this conversation as resolved
src/sbot.rs Outdated
@@ -19,6 +22,7 @@ use crate::utils;
// re-export types from kuska
pub use kuska_ssb::api::dto::content::SubsetQuery;
use kuska_ssb::rpc::RequestNo;
Owner

I suggest moving these up to where the other kuska_ dependencies are and grouping RequestNo into the existing kuska_ssb grouping. It seems like a minor difference but I've found this kind of organisation to help me a lot when navigating code I haven't written.

  1. std dependencies
  2. other dependencies (usually grouped in alphabetical order)
  3. local dependencies (ie. crate::)
I suggest moving these up to where the other `kuska_` dependencies are and grouping `RequestNo` into the existing `kuska_ssb` grouping. It seems like a minor difference but I've found this kind of organisation to help me a lot when navigating code I haven't written. 1. `std` dependencies 2. other dependencies (usually grouped in alphabetical order) 3. local dependencies (ie. `crate::`)
notplants marked this conversation as resolved
src/sbot.rs Outdated
@@ -229,1 +230,3 @@
&mut self,
/// Call the `createHistoryStream` RPC method
/// and return a Stream of Result<SsbMessageValue, GolgiError>
pub async fn create_history_stream<'a>(
Owner

I believe it's only necessary to add an explicit lifetime bound for return type (this compiles for me):

pub async fn create_history_stream(
    &mut self,
    id: String,
) -> Result<impl Stream<Item = Result<SsbMessageValue, GolgiError>> + '_, GolgiError> {
I believe it's only necessary to add an explicit lifetime bound for return type (this compiles for me): ```rust pub async fn create_history_stream( &mut self, id: String, ) -> Result<impl Stream<Item = Result<SsbMessageValue, GolgiError>> + '_, GolgiError> { ```
notplants marked this conversation as resolved
src/sbot.rs Outdated
@@ -235,0 +241,4 @@
/// Takes in an rpc request number, and a handling function (parsing results of type T),
/// and produces an async_std::stream::Stream
/// of results of type T where the handling functions is called
Owner

Small typo: handling functions -> handling function

Small typo: `handling functions` -> `handling function`
notplants marked this conversation as resolved
src/sbot.rs Outdated
@@ -235,0 +248,4 @@
///
/// * `req_no` - A `RequestNo` of the response to listen for
/// * `f` - A function which takes in an array of u8 and returns a Result<T, GolgiError>.
/// This is a function which parses the response from the RpcReader. T is a generic type,
Owner

Love this explanation!

Love this explanation!
notplants marked this conversation as resolved
src/sbot.rs Outdated
@@ -235,0 +250,4 @@
/// * `f` - A function which takes in an array of u8 and returns a Result<T, GolgiError>.
/// This is a function which parses the response from the RpcReader. T is a generic type,
/// so this parse function can return multiple possible types (String, json, custom struct etc.)
pub fn get_source_stream<'a, F, T>(&'a mut self, req_no: RequestNo, f: F) -> impl Stream<Item = Result<T, GolgiError>> + 'a
Owner

I was able to move this into src/utils.rs by using the following function signature:

pub fn get_source_stream<'a, F, T>(
    sbot_client: &'a mut Sbot,
    req_no: RequestNo,
    f: F,
) -> impl Stream<Item = Result<T, GolgiError>> + 'a
where
    F: Fn(&[u8]) -> Result<T, GolgiError> + 'a,
    T: Debug + serde::Deserialize<'a> + 'a,
{

Then L263 becomes:

let (id, msg) = sbot_client.rpc_reader.recv().await?;

And L273 requires a reference to body:

let item = f(&body)?;

Then in src/sbot.rs:

The rpc_reader field on Sbot needs to be public:

pub rpc_reader: RpcReader<TcpStream>,

And we replace self. with utils:: when calling get_source_stream:

let history_stream = utils::get_source_stream(self, req_id, utils::ssb_message_res_parse);
I was able to move this into `src/utils.rs` by using the following function signature: ```rust pub fn get_source_stream<'a, F, T>( sbot_client: &'a mut Sbot, req_no: RequestNo, f: F, ) -> impl Stream<Item = Result<T, GolgiError>> + 'a where F: Fn(&[u8]) -> Result<T, GolgiError> + 'a, T: Debug + serde::Deserialize<'a> + 'a, { ``` Then L263 becomes: ```rust let (id, msg) = sbot_client.rpc_reader.recv().await?; ``` And L273 requires a reference to `body`: ```rust let item = f(&body)?; ``` Then in `src/sbot.rs`: The `rpc_reader` field on `Sbot` needs to be public: ```rust pub rpc_reader: RpcReader<TcpStream>, ``` And we replace `self.` with `utils::` when calling `get_source_stream`: ```rust let history_stream = utils::get_source_stream(self, req_id, utils::ssb_message_res_parse); ```
notplants marked this conversation as resolved
src/sbot.rs Outdated
@@ -235,0 +258,4 @@
// we use the async_stream::stream macro to allow for creating a stream which calls async functions
// see https://users.rust-lang.org/t/how-to-create-async-std-stream-which-calls-async-function-in-poll-next/69760
let source_stream = stream! {
loop {
Owner

There's a formatting issue here. cargo fmt should do the trick (I have this setup to run every time I save).

There's a formatting issue here. `cargo fmt` should do the trick (I have this setup to run every time I save).
notplants marked this conversation as resolved
notplants added 4 commits 2022-01-05 18:59:52 +00:00
notplants added 1 commit 2022-01-05 19:00:28 +00:00
notplants added 1 commit 2022-01-05 19:02:24 +00:00
notplants added 1 commit 2022-01-05 19:06:06 +00:00
Author
Owner

@glyph I incoporated your comments, and this is now a fully functional PR and ready for official review to merge.

  • can call create_history_stream multiple times (ssb-stream-example is fully working)
  • moved get_source_stream to utils.rs and trimmed down the lifetimes
  • added get_sbot_connection function and made create_history_stream make use of it. I also still kept one sbot_connection as a field for Sbot which can be used by non-stream methods if desired (I don't know if this is a major optimization, but that's what I was thinking with that).

unfortunately cargo fmt seems to have added a bunch of whitespace to the PR which makes it a bit hard to read. will try to avoid that in the future!

the one thing I didn't do,

warning: avoid using `collect()` when not needed

... I would like to address this in a separate PR, where I update the about functions to use the stream code as well.

@glyph I incoporated your comments, and this is now a fully functional PR and ready for official review to merge. - can call create_history_stream multiple times (ssb-stream-example is fully working) - moved get_source_stream to utils.rs and trimmed down the lifetimes - added get_sbot_connection function and made create_history_stream make use of it. I also still kept one sbot_connection as a field for Sbot which can be used by non-stream methods if desired (I don't know if this is a major optimization, but that's what I was thinking with that). unfortunately cargo fmt seems to have added a bunch of whitespace to the PR which makes it a bit hard to read. will try to avoid that in the future! the one thing I didn't do, ``` warning: avoid using `collect()` when not needed ``` ... I would like to address this in a separate PR, where I update the about functions to use the stream code as well.
glyph approved these changes 2022-01-06 09:41:32 +00:00
glyph left a comment
Owner

Awesome, this look amazing. Thanks for the clear summary of the changes. Great work :)

One tiny thing I'm curious about is the underscore in front of this function name:

async fn _get_sbot_connection_helper(

But I don't want that to delay merging of this PR. Perhaps the underscore can be removed in one of your next PR's.

Merge at your leisure 🟢

Awesome, this look amazing. Thanks for the clear summary of the changes. Great work :) One tiny thing I'm curious about is the underscore in front of this function name: ```rust async fn _get_sbot_connection_helper( ``` But I don't want that to delay merging of this PR. Perhaps the underscore can be removed in one of your next PR's. Merge at your leisure 🟢
Author
Owner

@glyph async fn _get_sbot_connection_helper is the one method I didn't declare as public, because I thought its just a helper and not really part of the exposed API.
Some languages have a convention to indicate private methods and variables by prefixing with an underscore, but its just stylistic, I'm happy to remove it in the next PR if you prefer that.

@glyph `async fn _get_sbot_connection_helper` is the one method I didn't declare as public, because I thought its just a helper and not really part of the exposed API. Some languages have a convention to indicate private methods and variables by prefixing with an underscore, but its just stylistic, I'm happy to remove it in the next PR if you prefer that.
notplants merged commit a18e8a7866 into main 2022-01-06 15:31:57 +00:00
notplants deleted branch async-stream 2022-01-06 15:32:43 +00:00
Owner

Some languages have a convention to indicate private methods and variables by prefixing with an underscore, but its just stylistic, I'm happy to remove it in the next PR if you prefer that.

@notplants

Oh OK, that makes sense to me. I don't mind it staying in.

> Some languages have a convention to indicate private methods and variables by prefixing with an underscore, but its just stylistic, I'm happy to remove it in the next PR if you prefer that. @notplants Oh OK, that makes sense to me. I don't mind it staying in.
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: golgi-ssb/golgi#10