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