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

Add an async socket.io API #180

Merged
merged 15 commits into from
Jan 15, 2023
Merged

Add an async socket.io API #180

merged 15 commits into from
Jan 15, 2023

Conversation

1c3t3a
Copy link
Owner

@1c3t3a 1c3t3a commented Apr 12, 2022

This PR builds upon the async engine.io version and adds an async socket.io interface.
The interface is similar to the sync one and provides most methods in an async fashion. Note that the async version can currently only be used with tokio.

An Issue I stumbled upon while writing this, is the verbose way of defining callbacks (they need to return boxed futures).
I explained the issue here. From my point of view there is currently no better way than using async { .. }.boxed().

1c3t3a added 3 commits April 12, 2022 15:27
The new code and types added by this commit build
upon the structure of the asynchronous engine.io
version and expose a new AsyncClient and AsyncClientBuilder
@1c3t3a 1c3t3a requested a review from ctrlaltf24 April 12, 2022 13:46
@codecov
Copy link

codecov bot commented Apr 12, 2022

Codecov Report

Merging #180 (e8032e6) into main (13f8e96) will decrease coverage by 0.44%.
The diff coverage is 87.22%.

@@            Coverage Diff             @@
##             main     #180      +/-   ##
==========================================
- Coverage   91.31%   90.87%   -0.45%     
==========================================
  Files          31       37       +6     
  Lines        3764     4535     +771     
==========================================
+ Hits         3437     4121     +684     
- Misses        327      414      +87     
Impacted Files Coverage Δ
engineio/src/asynchronous/client/builder.rs 98.40% <ø> (ø)
socketio/src/asynchronous/client/ack.rs 0.00% <0.00%> (ø)
socketio/src/lib.rs 100.00% <ø> (ø)
socketio/src/socket.rs 87.39% <ø> (ø)
socketio/src/asynchronous/client/callback.rs 64.70% <64.70%> (ø)
socketio/src/asynchronous/client/client.rs 85.90% <85.90%> (ø)
socketio/src/asynchronous/socket.rs 86.30% <86.30%> (ø)
socketio/src/asynchronous/client/builder.rs 90.69% <90.69%> (ø)
engineio/src/asynchronous/async_socket.rs 80.50% <100.00%> (+2.51%) ⬆️
engineio/src/asynchronous/client/async_client.rs 96.48% <100.00%> (+0.73%) ⬆️
... and 7 more

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

Copy link
Collaborator

@ctrlaltf24 ctrlaltf24 left a comment

Choose a reason for hiding this comment

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

main concerns:

  • consider async callback in a separate feature
  • tests don't test to make sure the callbacks function
  • version bump

.github/workflows/build.yml Outdated Show resolved Hide resolved
engineio/src/asynchronous/transport.rs Outdated Show resolved Hide resolved
socketio/Cargo.toml Outdated Show resolved Hide resolved

[features]
default = ["async"] # TODO: remove this before merging, just here so that test run through
async = ["rust_engineio/async", "tokio", "futures-util", "async-stream"]
Copy link
Collaborator

Choose a reason for hiding this comment

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

suggestion: Put async callbacks behind a feature. Personally don't find async callbacks very ergonomic. Putting them behind a feature would allow us to explore other alternatives in a non-breaking fashion.

socketio/src/asynchronous/client/builder.rs Show resolved Hide resolved
socketio/src/asynchronous/generator.rs Show resolved Hide resolved
socketio/src/asynchronous/client/client.rs Outdated Show resolved Hide resolved
socketio/src/asynchronous/socket.rs Outdated Show resolved Hide resolved
socketio/src/asynchronous/socket.rs Show resolved Hide resolved
socketio/src/asynchronous/socket.rs Show resolved Hide resolved
@1c3t3a
Copy link
Owner Author

1c3t3a commented Apr 19, 2022

Current open points:

  • fix tests so that it is ensured that callbacks are called correctly
  • investigate UB that might arise when cloning the client
  • put async callbacks behind a feature flag

@1c3t3a
Copy link
Owner Author

1c3t3a commented Apr 28, 2022

@nshaaban-cPacket What alternatives to async callbacks would you consider? A possible alternative would be to somehow use the implementation of Stream to react to a packet and provide means to answer to the server directly. I don't like the way the callbacks work for a two main reasons:

  • The fact that the callback takes a Client as an argument is the root of the whole "client-needs-to-be-clonable" problem
  • Fns are hard to handle (especially in async context, where creation of Fns is difficult and counterintuitive) also they need to be boxed

@1c3t3a 1c3t3a requested a review from ctrlaltf24 April 28, 2022 21:11
@1c3t3a
Copy link
Owner Author

1c3t3a commented Apr 28, 2022

I added the async-callbacks feature flag. As there is currently no other alternative to async callbacks, I added it as a default feature when async is enabled. Anyhow the ClientBuilder's on<F: Fn(...) -> ...>(..) method is excluded when the flag is not enabled, giving an option to add different versions of the method (or other interfaces like trait implementations) as needed.

@ctrlaltf24
Copy link
Collaborator

@nshaaban-cPacket What alternatives to async callbacks would you consider? A possible alternative would be to somehow use the implementation of Stream to react to a packet and provide means to answer to the server directly.

Either:

  • streams (good in theory, bit of a pain to implement)
  • trait based callback. not exactly sure how this works internally, but the idea is provide a trait that implements handle_message (or similar), then pass that object into the client. then users can create their own structs with their own state that handle socket.io messages.

socketio/src/asynchronous/client/client.rs Outdated Show resolved Hide resolved
@ctrlaltf24
Copy link
Collaborator

Sorry it took so long to review, didn't see that you re-requested review.

fix tests so that it is ensured that callbacks are called correctly

May want to remove those tests that aren't actually testing so the coverage number is accurate.

Comment on lines +311 to +318
.map(|value| match value {
Value::String(ev) => ev,
_ => "message",
})
.unwrap_or("message")
.into(),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
.map(|value| match value {
Value::String(ev) => ev,
_ => "message",
})
.unwrap_or("message")
.into(),
.map(|value| match value {
Value::String(ev) => Event::from(ev),
_ => Event::Message,
})
.unwrap_or(Event::Message),

@JackFlukinger
Copy link

JackFlukinger commented Jun 9, 2022

Hey, FWIW, I tried out the async version and everything seemed to work except for event callbacks (not sure how comprehensively those are tested). The sync version of the library works fine with the same calling code, so I'm pretty sure it was not a server config problem.

@SSebo
Copy link
Contributor

SSebo commented Aug 16, 2022

@SSebo
Copy link
Contributor

SSebo commented Aug 16, 2022

hi, @1c3t3a :

The fact that the callback takes a Client as an argument is the root of the whole "client-needs-to-be-clonable" problem

https://github.com/SSebo/rust-socketio/blob/0b1890e5f7036105ef6661c4be7656af43f0ef16/socketio/src/client/client.rs#L37-L54

For reconnecting, the Client is cloneable, and the real client is inner: Arc<RwLock<Client>>. because we need to create a new socket to replace failed one, so the whole "client-needs-to-be-clonable" problem is acceptable I think.

What alternatives to async callbacks would you consider?

  • Callback in rust is also hard to access outer variable which must be wrapped with Arc<Lock> then move into the callback closure.
  • Register callback is normal in Javascript, but not very popular in Rust. The normal rust way usually register a sender, and spawn a new thread to recv new event, move necessary variable in, then process it.
  • If callback panic, it will kill the socket.io thread (or we spawn thread to handle callback), if sender is dead, socket.io thread can just skip it.

@ctrlaltf24
Copy link
Collaborator

What alternatives to async callbacks would you consider?

Another option is rather than sending the client in the callback, send an intermediary object that can uses a message channel (or similar means) to send messages out over the client. Inside the callback you don't really need to have the full powers of the client, just send messages/close connection/etc.

@1c3t3a 1c3t3a merged commit b6c58c1 into main Jan 15, 2023
@1c3t3a
Copy link
Owner Author

1c3t3a commented Jan 15, 2023

Thanks for the valuable discussion in this PR! I am going to merge this PR now, it comes with fairly good documentation of the new async part and I hope to get some feedback on the async API eventually (especially regarding the valuable points here raised by @SSebo and @nshaaban-cPacket). I am releasing this as an alpha version, since we want to be flexible with API changes, the async part is behind a feature flag either way :) Thanks to everyone that was involved in this, whether it was through testing or reviewing this PR! :)

@1c3t3a 1c3t3a deleted the async-socketio branch March 26, 2023 11:56
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.

4 participants