-
Couldn't load subscription status.
- Fork 668
New example (crypto and ai) - WORK IN PROGRESS, DO NOT MERGE #3037
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: develop
Are you sure you want to change the base?
Conversation
example/websocket/client/crypto-ai-ssl/historic_price_fetcher.cpp
Outdated
Show resolved
Hide resolved
example/websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp
Outdated
Show resolved
Hide resolved
| double price = 0; | ||
|
|
||
| // Design note: the json parsing could be done without using the exception | ||
| // interface, but the resulting code would be considerably more verbose. |
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.
we need to fix that then, or explore alternatives
| // so that the next async_* function is not called until *after* the previous asynchronous | ||
| // function is complete. However the strand is included since it makes our threading assumptions | ||
| // explicit for future developers. | ||
| boost::asio::strand<boost::asio::io_context::executor_type> strand_; |
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 purpose of a strand is not really to queue operations, it is to ensure that two operations do not execute concurrently. A subtle distinction.
"we could "get away" without using a strand," is not quite true, because of composed operations.
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.
Sorry, I don't understand. What do you mean by "composed operations" in this context?
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.
It needs an explanation too big for here
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.
Tanner Sansbury touches on it explicitly here https://stackoverflow.com/questions/12794107/why-do-i-need-strand-per-connection-when-using-boostasio/12801042#12801042
|
please rebase on |
| template<class Executor> | ||
| class historic_fetcher_op | ||
| { | ||
| enum class state { |
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 use of state seems unnecessary, the majority of completion handlers have unique signature and for those that don't (like write and read) we can define a tag type and use asio::prepend to prepend the tag value.
It would look like something like this:
struct on_write{};
struct on_read{};
// signature for write
void operator()(
Self& self
on_write,
, system::error_code ec
, std::size_t bytes_transferred);
// signature for read
void operator()(
Self& self
on_read,
, system::error_code ec
, std::size_t bytes_transferred);
// call site 1
beast::http::async_write(
ssl_stream_,
request_,
asio::prepend(std::move(self), on_write{})
);
// call site 2
beast::http::async_read(
ssl_stream_,
buffer_,
response_,
asio::prepend(std::move(self), on_read{})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.
interesting technique! I hadn't thought of this. You can also use the fauxroutines (in boost/asio/coroutine.hpp)
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 problem with using fauxroutines here is that we have a few incompatible function call signatures:
// resolve
void operator()(
Self& self
, system::error_code ec
, asio::ip::tcp::resolver::results_type results)
// connect
void operator()(
Self& self
, system::error_code ec
, asio::ip::tcp::resolver::results_type::endpoint_type ep)
// write/read
void operator()(
Self& self
, system::error_code ec
, std::size_t bytes_transferred);Although technically, we can only use the fauxroutine for the write/read operations, because that's where we have to deal with more than one operation.
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 downside of asio::prepend is that it creates multiple different function template instantiations of essentially the same asio implementation code
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 downside of asio::prepend is that it creates multiple different function template instantiations of essentially the same asio implementation code
If I'm not mistaken, the completion token type is unique for each composed type anyway (it's essentially the Self object type). So, unless we use different tag types for the same operation, like:
async_write(..., asio::prepend(std::move(self), on_write1{}));
async_write(..., asio::prepend(std::move(self), on_write2{}));there shouldn't be any difference.
| client_type& client_; | ||
| resolver_type& resolver_; | ||
| ssl_stream_type& ssl_stream_; | ||
|
|
||
| buffer_type& buffer_; | ||
| request_type& request_; | ||
| response_type& response_; |
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.
Would it be possible for historic_fetcher_op to just take a reference to historic_fetcher?
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.
that would be better, yes
| } break; | ||
| default: { | ||
| // This should not happen. | ||
| throw std::logic_error("unreachable"); |
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.
Composed operations should never throw exceptions, because the exception would be thrown from the call site of asio::io_context::run(). They should instead complete with an error (though in this case, it seems to be only for debugging purposes).
| request_.version(11); | ||
| request_.method(beast::http::verb::get); | ||
| request_.set(beast::http::field::host, client.get_host()); | ||
| request_.set(beast::http::field::user_agent, BOOST_BEAST_VERSION_STRING); |
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.
It is recommended not to perform preparation work that modifies the state of the io_object (historic_fetcher in this case) in the constructor. This is because we can create deferred operations that are planned to be executed later, for example:
auto op1 = fetcher.async_historic_fetch(asio::deferred);
auto op2 = fetcher.async_historic_fetch(asio::deferred);
// Both of these deferred operations have called the constructor of the composed operation,
// but neither has been scheduled for execution yet.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.
(me, taking notes; master class here)
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.
didn't know that...
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.
@ashtum I am slightly confused as to how to make the code completely robust here. Let's have a conversation in the morning if that's okay for you.
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 think he means to launch the async ops in a separate member function, such as operator()(Self& self) ? Example: https://github.com/vinniefalco/http_io/blob/b24d776d5ac6243099828a161f8f0f9265c1b356/example/client/visit/main.cpp#L112
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.
Yes, the preparation work should happen inside operator()(Self& self) instead of the constructor.
| , request_ | ||
| , response_) | ||
| , token | ||
| , exec); |
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.
Instead of taking exec in the argument we can use the ssl_stream_'s executor:
ssl_stream_.get_executor();In case of asio::async_compose you can just directly pass the io_object itself (ssl_stream_):
return asio::async_compose<
CompletionToken,
void(system::error_code)>(
historic_fetcher_op<Executor>(
*this
, resolver_
, ssl_stream_
, buffer_
, request_
, response_)
, token
, ssl_stream_);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 does raise the question of how best to parameterize the template types. should it be typename Executor? typename Stream?
| } break; | ||
| default: { | ||
| // This should not happen. | ||
| throw std::logic_error("unreachable"); |
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.
we have BOOST_UNREACHABLE for this (spelling?)
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.
Just what I was looking for, thanks (as std::unreachable is not available yet).
| , asio::ip::tcp::resolver::results_type results) | ||
| { | ||
| if (ec) { | ||
| state_ = state::error; |
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.
does this mean anything if we are completing?
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.
Probably not. When I put this in I thought I might have to check the state of the object after io.run finished.
However Mohammad's tag trick is neater than having a state (a full state machine is overkill for this and just makes the code harder to read)
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.
you should check out fauxroutines
| switch (state_) { | ||
| case state::resolving: { | ||
| // Set a timeout on the operation | ||
| beast::get_lowest_layer(ssl_stream_).expires_after(std::chrono::seconds(30)); |
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 believe this depends on the lowest layer being beast::basic_stream or something like that. The stream with the built-in timeout. I would prefer we didn't use that, because it is an overly complex and cumbersome design made obsolete by asio's new "cancelation" features. We don't want to teach it and we should probably deprecate it in beast somehow.
No description provided.