Connection state listener

Since v.0.5.1 RESTinio supports notification about client connection state changes. In v.0.6.0 this support was somewhat changed and extended and this chapter describes features of v.0.6.0.

RESTinio knows moments when a new connection from a client is accepted and when this connection is closed or upgraded (to websocket connection). By default, RESTinio doesn’t inform a user about those facts. But since v.0.5.1 a user can specify own connection state listener object for RESTinio server and RESTinio will call state_changed method of that object at the appropriate moments.

How to specify connection state listener?

There are three steps those needs to be done to specify own connection state listener to RESTinio server.

The first step is the creation of class with non-static state_changed method. This class can look like:

class my_state_listener {
   ...
public:
   ...
   void state_changed(
      const restinio::connection_state::notice_t & notice) {
      ...
   }
};

Please notice that state_changed gets a single argument of type restinio::connection_state::notice_t by const reference and returns void.

It is also important to mention that since v.0.6.0 state_changed can throw exceptions (in v.0.5.1 that method should be a noexcept one). If a connection listener throws an exception on accept or upgraded_to_websocket event then the new connection will be automatically closed by RESTinio.

The second step is the definition of my_state_listener as a type of connection state listener for RESTinio. A typedef with name connection_state_listener_t should be defined inside the server’s traits:

struct my_traits : public restinio::default_traits_t {
   using connection_state_listener_t = my_state_listener;
};

The third step is the creation an instance of state listener and passing it to the server’s settings:

restinio::run( restinio::on_this_thread<my_traits>()
      .port(8080)
      .address("localhost")
      .connection_state_listener(
         std::make_shared<my_state_listener>(...))
      .request_handler(...)
      ... );

The restinio::connection_state::notice_t class

An instance of restinio::connection_state::notice_t class passed to state_changed method contains all available information about the connection and its state. This class has the following interface:

class notice_t
{
   ...
public :
   ...

   //! Get the connection id.
   connection_id_t connection_id() const noexcept;

   //! Get the remote endpoint for the connection.
   endpoint_t remote_endpoint() const noexcept;

   //! Get the cause for the notification.
   cause_t cause() const noexcept;
};

Where restinio::connection_state::cause_t is the following variant type:

using cause_t = variant_t< accepted_t, closed_t, upgraded_to_websocket_t >;

Name variant_t is an alias for std::variant from C++17 or to a custom implementation of std::variant with as-close-as-possible interface (if fact, a wonderful variant-lite library is used by RESTinio).

To deal with variant type in C++14 RESTinio provides several backported by variant-lite functions: restinio::holds_alternative, restinio::get, restinio::get_if, restinio::visit.

Each type from cause_t variant is related to the appropriate cause and contains information related to that cause (if that information is available).

Types restinio::connection_state::closed_t and restinio::connection_state::upgraded_to_websocket_t are empty now, but they can be expanded in the future versions of RESTinio.

Type restinio::connection_state::accepted_t has the following interface:

class accepted_t final
{
   ...
public:
   [[NODISCARD]] bool is_tls_connection() const noexcept;

   template< typename Lambda >
   void try_inspect_tls( Lambda && lambda ) const;

   template< typename Lambda >
   decltype(auto) inspect_tls_or_throw( Lambda && lambda ) const;

   template< typename Lambda, typename T >
   T inspect_tls_or_default( Lambda && lambda, T && default_value ) const;
};

When and where state listener is called?

RESTinio uses just one instance of state listener object for all connections. It means that several calls to state_changed can be performed from different threads at the same time (if RESTinio is run on a thread pool).

RESTinio doesn’t serialize calls to state listener object if those calls are related to different connections. But calls for a single connection will be serialized in the sense that accepted notification will happen before closed or upgraded_to_websocket notification.

It is important to mention that if RESTinio is run on thread pool then notifications accepted and closed/upgraded_to_websocket for a particular connection can be issued on different threads.

Access to TLS-parameters

Since v.0.6.0 RESTinio supports access to TLS-related parameters for a connection listener. That access is provided via restinio::connection_state::accepted_t type when the connection listener is called after acception of a new connection.

Class restinio::connection_state::accepted_t allows to check is the new connection a TLS-connection:

void my_listener::state_changed(
   const restinio::connection_state::notice_t & notice)
{
   using restinio::connection_state::accepted_t;
   if(auto * accepted = restinio::get_if<accepted_t>(&notice.cause()))
   {
      if(accepted->is_tls_connection())
      {
         // This is a TLS-connection and we can handle its parameters.
         ...
      }
   }
}

Handling of TLS-parameters is possible in form of calling methods try_inspect_tls, inspect_tls_or_throw and inspect_tls_or_default of restinio::connection_state::accepted_t class. All those methods get a lambda/functor object with the following format:

some_type lambda(const restinio::connection_state::tls_accessor_t &);

The type restinio::connection_state::tls_accessor_t is defined in restinio/tls.hpp header file. That file is not included in restinio/core.hpp and should be included into your programm explicitely:

#include <restinio/core.hpp>
#include <restinio/tls.hpp>

Since v.0.6.0 type restinio::connection_state::tls_accessor_t has the following public interface:

class tls_accessor_t
{
public:
   [[nodiscard]] auto native_handle() const noexcept;
};

Where tls_accessor_t::native_handle() returns the value of the asio::ssl::stream::native_handle() method (the pointer to SSL struct from OpenSSL library).

Method accepted_t::try_inspect_tls calls specified lambda only if the new connection is TLS-connection, otherwise it does nothing. For example:

class my_cause_visitor_t {
   void operator()(const restinio::connection_state::accepted_t & cause) const {
      std::optional<std::string> user_name;
      cause.try_inspect_tls(
         [&](const restinio::connection_state::tls_accessor_t & tls_info) {
            // An attempt to extract name of the user from the certificate.
            user_name = extract_user_name_from_certificate(
                  tls_info.native_handle());
         });
      // If we have user_name...
      if(user_name) {
         ... // ...we can do something with it
      }
      ...
   }
   ...
};
void some_state_listener_t::state_changed(
   const restinio::connection_state::notice_t & notice) {
   ...
   restinio::visit(my_cause_visitor_t{...}, notice.cause());
}

Method accepted_t::inspect_tls_or_throw calls specified lambda if the new connection is TLS-connection or throws otherwise. If the lambda is called the value returned by that lambda is returned. For example:

class my_cause_visitor_t {
   void operator()(const restinio::connection_state::accepted_t & cause) const {
      // We expect only TLS-connections.
      std::string user_name = cause.inspect_tls_or_throw(
         [&](const restinio::connection_state::tls_accessor_t & tls_info) {
            // An attempt to extract name of the user from the certificate.
            return extract_user_name_from_certificate(tls_info.native_handle());
         });
      // Now we have the user_name and can do something with it.
      ...
   }
   ...
};
void some_state_listener_t::state_changed(
   const restinio::connection_state::notice_t & notice) {
   ...
   restinio::visit(my_cause_visitor_t{...}, notice.cause());
}

Method accepted_t::inspect_tls_or_default calls specified lambda if the new connection is TLS-connection or returns the default value passed to inspect_tls_or_default. If the lambda is called the value returned by that lambda is returned. For example:

class my_cause_visitor_t {
   void operator()(const restinio::connection_state::accepted_t & cause) const {
      // TLS-connection will be handled a specific way.
      std::string user_name = cause.inspect_tls_or_default(
         [&](const restinio::connection_state::tls_accessor_t & tls_info) {
            // An attempt to extract name of the user from the certificate.
            return extract_user_name_from_certificate(tls_info.native_handle());
         },
         // For all other connections that user name will be used.
         "anonymous"s);

      // Now we have the user_name and can do something with it.
      ...
   }
   ...
};
void some_state_listener_t::state_changed(
   const restinio::connection_state::notice_t & notice) {
   ...
   restinio::visit(my_cause_visitor_t{...}, notice.cause());
}

Limitation of the current RESTinio logic and max_pipelined_requests

The current version of RESTinio has the following logic:

  • RESTinio accepts a new connection and reads the first request;
  • when the first request received RESTinio calls request_handler and waits while response will be provided. Because RESTinio supports asynchronous processing there can be some time between calling request_handler and receiving a response from request_handler;
  • all that time RESTinio doesn’t read new data from the connection.

It means that if a connection is closed by client while request_handler handles the request, RESTinio won’t know about the disconnection. As a result connection state listener won’t be called.

The following small example allows to see that situation:

#include <restinio/core.hpp>

struct my_connection_listener
{
   void
   state_changed(const restinio::connection_state::notice_t & notice) noexcept
   {
      using namespace restinio::connection_state;
      if(restinio::holds_alternative<accepted_t>(notice.cause()))
         std::cout << notice.connection_id() << " -- accepted" << std::endl;
      else if(restinio::holds_alternative<closed_t>(notice.cause()))
         std::cout << notice.connection_id() << " -- closed" << std::endl;
   }
};

int main()
{
   struct my_traits : public restinio::default_single_thread_traits_t
   {
      using logger_t = restinio::single_threaded_ostream_logger_t;
      using connection_state_listener_t = my_connection_listener;
   };

   std::vector< restinio::request_handle_t > requests;

   restinio::run(
      restinio::on_this_thread< my_traits >()
         .port(8080)
         .address("localhost")
         .handle_request_timeout(std::chrono::hours{24})
         .connection_state_listener(
            std::make_unique<my_connection_listener>())
//         .max_pipelined_requests(4) // (1)
         .cleanup_func([&] {
               requests.clear();
            })
         .request_handler([&](auto req) {
            requests.push_back(req);
            return restinio::request_accepted();
         }));

   return 0;
}

Just compile and run that example. Then call curl -v http://localhost:8080 and then terminate curl by Ctrl+C. There won’t be a notification about disconnection. It is because RESTinio reads one incoming request and stops reading connection until the request read will be processed. No read operations mean that RESTinio can’t detect a disconnection.

But RESTinio can read and process more that one request from a connection. This feature is called pipelined requests. There is setting max_pipelined_requests that specified how many requests can be read from a connection. By default, this setting is equal to 1. But it can be changed.

So if the line (1) in the example above will be uncommented then RESTinio will continue reading the connection after extracting the first incoming request. That allows to detect a disconnection and connection state listener will be invoked when the disconnection is detected.

As the conclusion: if you want to receive notifications about disconnections while requests are being processing then increase the value of max_pipelined_requests settings.

It can look like a flaw in RESTinio and maybe it is. But this is a complex question and we don’t find an appropriate solution yet. Maybe RESTinio’s logic will be changed in some future version. But for v.0.6.0 a user has to deal with max_pipelined_requests value.

Thread safety of state listener

If RESTinio is run on a thread pool then calls of state_changed can be made from several threads. Moreover, several calls to state_changed can be performed at the same time.

It’s a user’s task to make a state listener thread-safe.

Performance impact

Usage of a custom state listener can have some performance impact. Its value depends on the logic of listener object, thread-safety mechanisms used in the implementation of a listener object and the contention on the listener object in a multithreaded application. So it’s the user’s responsibility to make state listener object as quick as possible.

If the default value for connection_state_listener_t is used (e.g. connection_state_listener_t in server traits has the default value of restinio::connection_state::noop_listener_t) then there won’t be any performance impact because all listener-related code will be eliminated by C++ compiler (it’s a consequence of specializations inside RESTinio’s code for noop_listener_t case).