Connection state listener

Since v.0.5.1 RESTinio supports notification about client connection state changes.

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) noexcept {
      ...
   }
};

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 state_changed is noexcept-method.

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 enumeration:

enum class cause_t
{
   //! Connection from a client has been accepted.
   accepted,
   //! Connection from a client has been closed.
   //! Connection can be closed as result of an error or as a normal
   //! finishing of request handling.
   closed,
   //! Connection has been upgraded to WebSocket.
   //! State listener won't be invoked for that connection anymore.
   upgraded_to_websocket
};

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.

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/all.hpp>

struct my_connection_listener
{
   void
   state_changed(const restinio::connection_state::notice_t & notice) noexcept
   {
      switch(notice.cause())
      {
      case restinio::connection_state::cause_t::accepted:
         std::cout << notice.connection_id() << " -- accepted" << std::endl;
      break;

      case restinio::connection_state::cause_t::closed:
         std::cout << notice.connection_id() << " -- closed" << std::endl;
      break;

      default: ;
      }
   }
};

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.5.1 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).