drogon icon indicating copy to clipboard operation
drogon copied to clipboard

Ability to detect client disconnection

Open dkalinowski opened this issue 1 year ago • 12 comments

Is your feature request related to a problem? Please describe. In our project we are trying to use Drogon for very long requests (up to 10 minutes long). While it is possible to detect client disconnection when using async streaming responses (by checking stream->send() return value), it is impossible to detect client disconnection early using standard response. It makes it impossible to stop long running workloads and free server resources upon client disconnection.

Describe the solution you'd like It would be nice to be able to install client disconnection callback. We could use the callback to execute workload stopping mechanism on our end.

Describe alternatives you've considered There is no alternative I know of.

dkalinowski avatar Oct 22 '24 09:10 dkalinowski

https://github.com/drogonframework/drogon/pull/2191

fantasy-peak avatar Oct 25 '24 22:10 fantasy-peak

#2191

This PR added ability to check the connection state in case we have access to http request object. The issue is related to scenario when there is no access to request object, during streaming, example: https://github.com/drogonframework/drogon/blob/master/examples/async_stream/main.cc#L13-L24

@fantasy-peak

dkalinowski avatar Oct 29 '24 09:10 dkalinowski

    app().registerHandler(
        "/stream",
        [](const HttpRequestPtr & ptr,
           std::function<void(const HttpResponsePtr &)> &&callback) {
            std::thread([=] {
                        while (true)
                        {
                            std::lock_guard lk(mm);
                            std::cout <<"aaaaaaaaaa:" << std::boolalpha
                                      << ptr->connected() << std::endl;
                            std::this_thread::sleep_for(
                                std::chrono::seconds(2));
                        }
            }).detach();

➜  build git:(check-conn) ✗ ./examples/async_stream
20241030 01:19:13.772283 UTC 11147 INFO  Server running on 127.0.0.1:8848 - main.cc:100
20241030 01:19:16.940796 UTC 11148 DEBUG [makeHeaderString] send stream with transfer-encoding chunked - HttpResponseImpl.cc:547
aaaaaaaaaa:true
aaaaaaaaaa:true
aaaaaaaaaa:true

nqf avatar Oct 30 '24 01:10 nqf

You can still use HttpRequestPtr

nqf avatar Oct 30 '24 01:10 nqf

@nqf even if you can check connection status the way you described, we still have no way to get disconnection information when workload is blocking. The example would be running your example with no while (true), but single blocking operation. The solution would be to have an ability to install disconnection callback in which the caller could define logic to stop the blocking workload.

What do you think about it?

dkalinowski avatar Nov 04 '24 09:11 dkalinowski

Do you mean that drogon's worker thread is blocked?

fantasy-peak avatar Nov 04 '24 09:11 fantasy-peak

Yes, consider the example be slightly modified:

    app().registerHandler(
        "/stream",
        [](const HttpRequestPtr & ptr,
           std::function<void(const HttpResponsePtr &)> &&callback) {
            std::thread([=] {
              my_workload->run(); // blocking for 10 minutes
            }).detach();

If we would be able to set up disconnection callback, we could run my_workload->stop() and cancel it to save resources

dkalinowski avatar Nov 04 '24 09:11 dkalinowski

add a new mr

fantasy-peak avatar Nov 06 '24 06:11 fantasy-peak

@dkalinowski HttpRequest add setCloseCallback api may solve your problem

zjd1988 avatar Dec 13 '24 08:12 zjd1988

Thanks @zjd1988, it solved my problem. #2204 PR introduces getter for TcpConnection ptr which allows installing custom close callback per connection. Can we merge it? @zjd1988 @fantasy-peak

dkalinowski avatar Jan 08 '25 11:01 dkalinowski

@zjd1988 @fantasy-peak It resolved my issue, however issue still persists when I don't move with the workload to separate thread.

What do I mean by that:

It works, when I define endpoint like this:

drogon::app().setDefaultHandler([this](const drogon::HttpRequestPtr& req, std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
  myThreadPool->Schedule([req, callback = std::move(callback)]() mutable {
    longBlockingWorkload(); 
  }
}

It doesnt work when I define endpoint like this:

drogon::app().setDefaultHandler([this](const drogon::HttpRequestPtr& req, std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
  longBlockingWorkload(); 
}

^ This way I only get disconnection callback executed when server is done with the request (after the workload)

Can I do something about it? In our project we decided to get rid of separate thread pool because it greatly increased RAM usage. Staying inside drogon threads did not have such issue. So, now we have to either accept there is no disconnection API or high RAM utilization.

dkalinowski avatar Feb 28 '25 14:02 dkalinowski

You can't call a blocking task in a drogon worker thread, which blocks drogon from processing other tasks

fantasy-peak avatar Feb 28 '25 21:02 fantasy-peak