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

Coroutinize http client code #2429

Open
wants to merge 28 commits into
base: master
Choose a base branch
from

Conversation

xemul
Copy link
Contributor

@xemul xemul commented Sep 12, 2024

The code looks nicer and with much less indentation depth that way

@xemul xemul requested review from tchaikov and nyh September 12, 2024 12:00
And brush up the if-else nesting a little bit to avoid co_return-s

Signed-off-by: Pavel Emelyanov <[email protected]>
For simplicity, wrap the entrance `if` around.

Signed-off-by: Pavel Emelyanov <[email protected]>
And leave indentation broken until next patch

Signed-off-by: Pavel Emelyanov <[email protected]>
The former helper was introduced to reduce the netsing of .then-chains.
Now when it's a coroutinie, it's better be squashed with its only
caller.

Signed-off-by: Pavel Emelyanov <[email protected]>
Same as previous patch -- the former method was a way to reduce
indentation level for the latter. Now they look good enough when
squashed.

Signed-off-by: Pavel Emelyanov <[email protected]>
There's a trickery there. After _closed future is resolved "this" may
become freed already. So to make the last log message printed, the
this->local_address was saved into .then capture. When coroutinized,
that field is kept on stack.

Signed-off-by: Pavel Emelyanov <[email protected]>
There's a trickery here. After con->close() is resolved, the con may be
the last pointer holding the connection alive. Before this patch, it was
saved on .finally capture, after it -- it's preserved as on-stack
variable.

Signed-off-by: Pavel Emelyanov <[email protected]>
In its .then-chain form the method calls itself to emulate "goto again"
without goto. To do the same with coroutines, wrap it into a loop.

Signed-off-by: Pavel Emelyanov <[email protected]>
This method closes all its connection by calling itself until the pool
is empty. With coroutines it is done with the plain loop.

Signed-off-by: Pavel Emelyanov <[email protected]>
@@ -191,7 +191,7 @@ private:
future<> shrink_connections();

template <std::invocable<connection&> Fn>
auto with_connection(Fn&& fn, abort_source*);
futurize_t<std::invoke_result_t<Fn, connection&>> with_connection(Fn fn, abort_source*);
Copy link
Contributor

Choose a reason for hiding this comment

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

heh, i believe Avi would like this change.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It has less than 4 closing corner braces in a row, so not that bad

Meanwhile in scylla (scroll right):

test/boost/user_function_test.cc:769:        e.execute_cql("CREATE FUNCTION my_func(val int) CALLED ON NULL INPUT RETURNS map<int, frozen<set<frozen<list<frozen<tuple<text, bigint>>>>>>> LANGUAGE Lua AS  \

😱

Copy link
Contributor

Choose a reason for hiding this comment

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

ah, i meant, he always prefers an explicit return type. this one make it more explicit. so..

src/http/client.cc Outdated Show resolved Hide resolved
@@ -341,13 +341,11 @@ future<> client::do_make_request(connection& con, request& req, reply_handler& h
connection::reply_ptr reply = co_await con.do_make_request(req);
auto& rep = *reply;
if (expected.has_value() && rep._status != expected.value()) {
if (!http_log.is_enabled(log_level::debug)) {
throw httpd::unexpected_status_error(reply->_status);
if (http_log.is_enabled(log_level::debug)) {
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 prefer early return for less indent. but don't feel strong either way.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is to de-duplicate two throw-s of the same exception

Copy link
Contributor

@tchaikov tchaikov left a comment

Choose a reason for hiding this comment

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

left some comments.

src/http/client.cc Show resolved Hide resolved
When reply contains unexpected status, client may print its body in
logs. Before being coroutinized it was two disctinct chains, with
coroutines it makes sense to rewrap them a little bit.

Signed-off-by: Pavel Emelyanov <[email protected]>
It used to call itself to "goto again". With coroutines it won't work as
nice, neither it looks good enough with the outer loop :( So use goto
explicitly.

Signed-off-by: Pavel Emelyanov <[email protected]>
It makes some non-trivial decisions on whether or to retry the request
in the catch block, so the conversion is a bit hairy.

Signed-off-by: Pavel Emelyanov <[email protected]>
@xemul
Copy link
Contributor Author

xemul commented Sep 12, 2024

upd:

  • removed unused ex variable
  • simplified retry coroutinization in client::do_make_request (con-less overload) patch

Copy link
Contributor

@tchaikov tchaikov left a comment

Choose a reason for hiding this comment

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

lgtm

@gleb-cloudius
Copy link
Contributor

As a general note when I tried to co-routinize cql server in Scylla I did not find a way to do so without introducing more allocations when requests are pipelined. With continuations if we read more than one request from a socket all of them can be processed without allocating continuations since futures will be ready. With co-routins we always allocate.

@avikivity
Copy link
Member

With [[clang::coro_elide_safe]] nested coroutines can share the coroutine frame. However it only arrives in clang 20 and isn't standardized.

return _write_buf.write(req.content);
} else {
return make_ready_future<>();
co_await _write_buf.write(req.content);
Copy link
Member

Choose a reason for hiding this comment

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

Doesn't this add an allocation in the common case that the buffer isn't full?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, but all users we have use lambda body writer and don't step on this branch. But lambda body writer also adds allocation here

if (req.get_header("Expect") == "") {
return make_ready_future<reply_ptr>(nullptr);
if (req.get_header("Expect") != "") {
co_await _write_buf.flush();
Copy link
Member

Choose a reason for hiding this comment

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

The common case is, that Expect will be empty, no? so we add an allocation here.

Please measure the performance of the series.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The common case is, that Expect will be empty, no? so we add an allocation here.

The allocation is unavoidable here anyway, at the end this method does co_await recv_reply() and it will wait for it

Please measure the performance of the series.

ok

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Please measure the performance of the series.

With #2442 , the test makes 1M requests in

  • master: 3.40 +/- 0,02 seconds (min 3,37 / max 3,50)
  • this PR: 3,48 +/- 0,02 seconds (min 3,45 / max 3,62)

Copy link
Member

Choose a reason for hiding this comment

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

Please report allocs/op

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  • master: 45 allocs/req
  • this PR: 46 allocs/req

@nyh
Copy link
Contributor

nyh commented Sep 17, 2024

Hi @xemul,

When you created a 28-patch series which must have taken you many many hours to create, please spend another 10 minutes trying to explain your motivation for doing this. Was it just the generic "coroutines are nicer" sentiment that we all shared, or something specific which bothered you?

Also, I see a lot of discussion regarding the lower performance of the coroutine code, so I wonder if you made an attempt to benchmark the http server somehow to see if there's anything we need to worry about.

@xemul
Copy link
Contributor Author

xemul commented Sep 18, 2024

Hi @xemul,

When you created a 28-patch series which must have taken you many many hours to create, please spend another 10 minutes trying to explain your motivation for doing this. Was it just the generic "coroutines are nicer" sentiment that we all shared, or something specific which bothered you?

No, nothing specific. It just looks much nicer and simpler this way.

Also, I see a lot of discussion regarding the lower performance of the coroutine code, so I wonder if you made an attempt to benchmark the http server somehow to see if there's anything we need to worry about.

I made a silly test with existing http client demo, but it's not very representative. I'll update the discussion thread with better measurement

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.

5 participants