Skip to content

C++ client low level API #8420

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

Merged
merged 19 commits into from
Mar 25, 2019
Merged

C++ client low level API #8420

merged 19 commits into from
Mar 25, 2019

Conversation

@BrennanConroy BrennanConroy added the area-signalr Includes: SignalR clients and servers label Mar 11, 2019
@BrennanConroy
Copy link
Member Author

Ping, any feedback?

No feedback is fine too, just want people to take a look :)

One thing I've been thinking about is accepting the http_client as a unique_ptr to the hub_connection, that way the lifetime is well known and there wont be any weird issues with the http_client and connection being in the same scope but destructors running at different times causing dangling pointer usage.

Copy link
Contributor

@analogrelay analogrelay left a comment

Choose a reason for hiding this comment

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

Looks pretty good

{
public:
int status_code = 0;
std::string content;
Copy link
Contributor

Choose a reason for hiding this comment

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

Are we going to support SSE? If not, and I think we can probably do without it, this should be fine. If we do want to support it, we'll need this to be a stream of some kind.

Copy link
Member

Choose a reason for hiding this comment

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

We just need to make sure that it can be added.

Copy link
Member

Choose a reason for hiding this comment

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

We talked about using ostream for this and istream for the request content. The problem is that only offers blocking and polling APIs. The alternative is using Asio's AsyncRead/WriteStream.

Since we're only aiming for source compatibility initially instead of some stable C ABI, we can probably just add an async stream field to http_request/response later and gracefully skip SSE if the http_client that doesn't use the field. We'd need a new send overload that calls the callback prior to buffering the entire response anyway.


virtual ~websocket_client() {};
virtual void receive(std::function<void(std::string, std::exception_ptr)> callback) = 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

From the naming and structure I presume this calls the callback once for each call to receive, when a single frame is received? Another alternative would be a on_recieved API that registers a callback which will be called once for each frame that arrives (and we'd do the looping ourselves).


logger m_logger;
virtual void on_receive(std::function<void(std::string, std::exception_ptr)> callback) = 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd expect to use the same API in websocket_client as we do here, so maybe we should change websocket_client to use on_receive as well...

Copy link
Member Author

Choose a reason for hiding this comment

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

The question is, do we expect people who provide their own websocket impl to do the receive loop and call our callback, or do we do the receive loop in the transport and just call receive on their impl?

@analogrelay
Copy link
Contributor

One thing I've been thinking about is accepting the http_client as a unique_ptr to the hub_connection, that way the lifetime is well known and there wont be any weird issues with the http_client and connection being in the same scope but destructors running at different times causing dangling pointer usage.

Did you say... ownership?

image

@analogrelay
Copy link
Contributor

analogrelay commented Mar 15, 2019

I do think that's a good idea though. Big fan of unique_ptr when we feel like it makes sense, and I think it makes sense here.

http_method method;
std::map<std::string, std::string> headers;
std::string content;
std::chrono::seconds timeout;
Copy link
Member

Choose a reason for hiding this comment

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

Maybe timeout should be a separate parameter on send. Or better yet, define a single-method CTS-like class that we return from send instead.


if (!cts.get_token().is_canceled())
{
transport->receive_loop(cts);
Copy link
Member

Choose a reason for hiding this comment

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

Could this stack dive if enough messages were buffered in memory that websocket_client->receive repeatedly called its callback inline?

Copy link
Member Author

Choose a reason for hiding this comment

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

Definitely could. I believe we talked about this and decided to do it this way for now and re-evaluate later if we need to dispatch.

Copy link
Member

Choose a reason for hiding this comment

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

If there's a risk, I figure it's easy enough to turn this into a trampoline to avoid the problem. I think it would look something like this.

auto calling_receive = true;
while (calling_receive) {
    websocket_client->receive([weak_transport, cts, logger, websocket_client, calling_receive](std::string message, std::exception_ptr exception)
        {
           // ...
           if (!cts.get_token().is_canceled())
           {
                if (calling_receive)
                {
                    calling_receive = false
                }
                else
                {
                    transport->receive_loop(cts);
                }
            }
        });

    calling_receive = !calling_receive;
}

Copy link
Member Author

Choose a reason for hiding this comment

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

Something like that would work, but we still need to jump off the main thread for the start of the receive loop in order to avoid blocking start. Right now I'm relying on the transport dispatching, like we talked about in the meeting.

Copy link
Member

Choose a reason for hiding this comment

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

What's the main thread? Whatever thread you call connection.start() on?

Do we jump off the main thread currently before calling websocket_client->receive()? Or does this design prevent us from jumping off the main thread in the future?

I would think the trampoline is strictly better than the recursive version because it avoids potentially stack diving.

Copy link
Member Author

Choose a reason for hiding this comment

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

What's the main thread? Whatever thread you call connection.start() on?

Correct.

Do we jump off the main thread currently before calling websocket_client->receive()? Or does this design prevent us from jumping off the main thread in the future?

We don't do anything threading related yet, except in the http_client and websocket_client as those use pplx. We can add it whenever though.

I would think the trampoline is strictly better than the recursive version because it avoids potentially stack diving.

I agree.

@BrennanConroy BrennanConroy marked this pull request as ready for review March 19, 2019 23:14
@BrennanConroy BrennanConroy requested a review from jkotalik March 20, 2019 00:09
}

std::shared_ptr<connection_impl> connection_impl::create(const std::string& url, trace_level trace_level, const std::shared_ptr<log_writer>& log_writer,
std::unique_ptr<web_request_factory> web_request_factory, std::unique_ptr<transport_factory> transport_factory)
std::unique_ptr<http_client> http_client, std::unique_ptr<transport_factory> transport_factory)
Copy link
Contributor

Choose a reason for hiding this comment

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

If someone passes a null http_client here, we should probably throw here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

{
try
{
task.get();
Copy link
Contributor

Choose a reason for hiding this comment

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

I think I need a bit more context on what this method is doing. Why do you need to complete the task provided here? Is the callback here only used for exception handling or is it required?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is a point in time thing as we're still using pplx::task in our code.

What this is doing, is observing any exceptions from the completed task so we don't crash the process, and then forwarding the exception to the new callback based APIs

}
catch (...)
{
callback(std::current_exception());
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not convinced this pattern for Try/Catch is good. Why not catch a std::exception& and pass the ref into the callback? That way you don't need to rethrow to get the exception.

Copy link
Contributor

@jkotalik jkotalik left a comment

Choose a reason for hiding this comment

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

LGTM from the perspective of non-signalR dev. IMO let's try to get this in sooner than later.

@BrennanConroy
Copy link
Member Author

@BrennanConroy BrennanConroy requested a review from Tratcher as a code owner March 25, 2019 16:31
@analogrelay analogrelay added this to the 3.0.0-preview4 milestone Mar 25, 2019
@BrennanConroy BrennanConroy merged commit 969c72a into master Mar 25, 2019
@BrennanConroy BrennanConroy deleted the brecon/lowapi branch March 25, 2019 22:13
@davidfowl
Copy link
Member

👏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-signalr Includes: SignalR clients and servers
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants