c++boostboost-asio

ssl::stream::shutdown sometimes does not complete


I want to close the TLS connection gracefully. So before closing the socket I call ssl::stream::shutdown. I call ssl::stream::shutdown in the session destructor and at that moment I have no active async operations.

However, sometimes ssl::stream::shutdown hangs and does not return. Because of this, the server remains waiting and cannot close the connection with the client.

Why can ssl::stream::shutdown hang and how to solve this problem? This is how I call shutdown

struct ServerSession : std::enable_shared_from_this<ServerSession> 
{
    // ...
    ~ServerSession() 
    {
        error_code ec;
        s.shutdown(ec); // synchronous shutdown
        std::cout << "Shutdown: " << ec.message() << std::endl;
    }
    // ...

  private:
    ssl::context             ctx_;
    ssl::stream<tcp::socket> s;
};

Solution

  • So, mulling things over, I came to this elegant idea to make the shutdown async AND time limited without heroics. Well, without manual heroics.

    Referring to the example from the other answer where we had:

    ~ClientSession() {
        error_code ec;
        s.shutdown(ec); // synchronous shutdown
        std::cout << "Shutdown: " << ec.message() << std::endl;
    }
    

    You could replace it with the equivalent async timelimited approach:

    ~ClientSession() {
        // auto  token = asio::detached;
    
        using keep_t = std::pair<ssl::context, ssl::stream<tcp::socket>>;
        auto keep    = std::make_unique<keep_t>(std::move(ctx_),std::move(s_));
        auto& [_, s] = *keep; // avoid UB by getting the reference before the move
        s.async_shutdown(consign(asio::cancel_after(50ms, token), std::move(keep)));
    
        // Note: The stream will be destroyed only after the shutdown completes (keep)
    }
    

    There's a bit of subtlety there:

    Of course, to get it actually equivalent to the synchronous original version:

    auto token = [](error_code ec) { std::cout << "Shutdown: " << ec.message() << std::endl; };
    

    Listing

    Live On Coliru

    #define BOOST_ASIO_ENABLE_HANDLER_TRACKING
    #include <boost/asio.hpp>
    #include <boost/asio/ssl.hpp>
    #include <deque>
    #include <iostream>
    
    using namespace std::literals;
    namespace asio = boost::asio;
    namespace ssl  = asio::ssl;
    using asio::ip::tcp;
    using boost::system::error_code;
    
    struct ClientSession : std::enable_shared_from_this<ClientSession> {
        ClientSession(asio::any_io_executor ex) : s_{std::move(ex), ctx_} {
            s_.lowest_layer().connect({{}, 8989});
            s_.handshake(ssl::stream_base::client); // synchronous for simplicity
        }
    
        void start() { do_read_loop(); }
    
        void send(std::string msg) {
            post(s_.get_executor(), [this, self = shared_from_this(), m = std::move(msg)]() mutable {
                outbox_.push_back(std::move(m));
                if (outbox_.size() == 1)
                    do_write_loop();
            });
        }
    
        void shutdown() {
            post(s_.get_executor(), [this, self = shared_from_this()]() {
                s_.lowest_layer().cancel();
            });
        }
    
        ~ClientSession() {
            // auto  token = asio::detached;
            auto token   = [](error_code ec) { std::cout << "Shutdown: " << ec.message() << std::endl; };
            using keep_t = std::pair<ssl::context, ssl::stream<tcp::socket>>;
            auto keep    = std::make_unique<keep_t>(std::move(ctx_),std::move(s_));
            auto& [_, s] = *keep; // avoid UB by getting the reference before the move
            s.async_shutdown(consign(asio::cancel_after(50ms, token), std::move(keep)));
    
            std::cout << "ClientSession destroyed, waiting for shutdown to complete..." << std::endl;
            // Note: The stream will be destroyed only after the shutdown completes (keep)
        }
    
      private:
        ssl::context             ctx_{ssl::context::sslv23};
        ssl::stream<tcp::socket> s_;
        std::array<char, 1024>   buffer_;
        std::deque<std::string>  outbox_;
    
        void do_read_loop() {
            s_.async_read_some(asio::buffer(buffer_), [this, self = shared_from_this()](error_code ec, size_t n) {
                if (ec)
                    std::cout << "Read error: " << ec.message() << std::endl;
                else {
                    std::cout << "Read " << n << " bytes: " << std::string(buffer_.data(), n) << std::endl;
                    do_read_loop(); // continue reading
                }
            });
        }
    
        void do_write_loop() {
            if (outbox_.empty())
                return;
    
            asio::async_write( //
                s_, asio::buffer(outbox_.front()), [this, self = shared_from_this()](error_code ec, size_t n) {
                    if (ec) {
                        std::cout << "Write error: " << ec.message() << std::endl;
                    } else {
                        outbox_.pop_front();
                        std::cout << "Wrote " << n << " bytes." << std::endl;
                    }
                    do_write_loop(); // continue writing
                });
        }
    };
    
    int main() {
        asio::io_context ioc(1); // single thread
    
        {
            auto sess = make_shared<ClientSession>(ioc.get_executor()); //
            sess->start();
    
            ioc.run_for(1s);
            sess->send("Hello, world!\n"); // send a message
    
            ioc.run_for(3s);
            sess->send("Bye, world!\n"); // send a message
            sess->shutdown(); // cancellation
        } // sess goes out of scope here, so destructor will run once cancellation(s) complete
    
        // run to completion
        ioc.run();
    }
    

    Printing e.g.

    Wrote 14 bytes.
    Wrote 12 bytes.
    Read error: Operation canceled
    ClientSession destroyed, waiting for shutdown to complete...
    Shutdown: Success
    

    And the handler-visualization:

    enter image description here