mirror of
https://gitlab.isc.org/isc-projects/kea
synced 2025-08-29 13:07:50 +00:00
[#26,!106] Guard HTTP client connect with timeout.
This commit is contained in:
parent
7f399ecae1
commit
ba4fe0c9e8
@ -122,8 +122,11 @@ public:
|
|||||||
/// @param request_timeout Request timeout in milliseconds.
|
/// @param request_timeout Request timeout in milliseconds.
|
||||||
/// @param callback Pointer to the callback function to be invoked when the
|
/// @param callback Pointer to the callback function to be invoked when the
|
||||||
/// transaction completes.
|
/// transaction completes.
|
||||||
|
/// @param connect_callback Pointer to the callback function to be invoked when
|
||||||
|
/// the client connects to the server.
|
||||||
void doTransaction(const HttpRequestPtr& request, const HttpResponsePtr& response,
|
void doTransaction(const HttpRequestPtr& request, const HttpResponsePtr& response,
|
||||||
const long request_timeout, const HttpClient::RequestHandler& callback);
|
const long request_timeout, const HttpClient::RequestHandler& callback,
|
||||||
|
const HttpClient::ConnectHandler& connect_callback);
|
||||||
|
|
||||||
/// @brief Closes the socket and cancels the request timer.
|
/// @brief Closes the socket and cancels the request timer.
|
||||||
void close();
|
void close();
|
||||||
@ -173,9 +176,10 @@ private:
|
|||||||
/// If the connection is successfully established, this callback will start
|
/// If the connection is successfully established, this callback will start
|
||||||
/// to asynchronously send the request over the socket.
|
/// to asynchronously send the request over the socket.
|
||||||
///
|
///
|
||||||
/// @param request_timeout Request timeout specified for this transaction.
|
/// @param Pointer to the callback to be invoked when client connects to
|
||||||
|
/// the server.
|
||||||
/// @param ec Error code being a result of the connection attempt.
|
/// @param ec Error code being a result of the connection attempt.
|
||||||
void connectCallback(const long request_timeout,
|
void connectCallback(HttpClient::ConnectHandler connect_callback,
|
||||||
const boost::system::error_code& ec);
|
const boost::system::error_code& ec);
|
||||||
|
|
||||||
/// @brief Local callback invoked when an attempt to send a portion of data
|
/// @brief Local callback invoked when an attempt to send a portion of data
|
||||||
@ -269,6 +273,8 @@ public:
|
|||||||
/// be stored.
|
/// be stored.
|
||||||
/// @param request_timeout Requested timeout for the transaction.
|
/// @param request_timeout Requested timeout for the transaction.
|
||||||
/// @param callback Pointer to the user callback for this request.
|
/// @param callback Pointer to the user callback for this request.
|
||||||
|
/// @param connect_callback Pointer to the user callback invoked when
|
||||||
|
/// the client connects to the server.
|
||||||
///
|
///
|
||||||
/// @return true if the request for the given URL has been retrieved,
|
/// @return true if the request for the given URL has been retrieved,
|
||||||
/// false if there are no more requests queued for this URL.
|
/// false if there are no more requests queued for this URL.
|
||||||
@ -276,7 +282,8 @@ public:
|
|||||||
HttpRequestPtr& request,
|
HttpRequestPtr& request,
|
||||||
HttpResponsePtr& response,
|
HttpResponsePtr& response,
|
||||||
long& request_timeout,
|
long& request_timeout,
|
||||||
HttpClient::RequestHandler& callback) {
|
HttpClient::RequestHandler& callback,
|
||||||
|
HttpClient::ConnectHandler& connect_callback) {
|
||||||
// Check if there is a queue for this URL. If there is no queue, there
|
// Check if there is a queue for this URL. If there is no queue, there
|
||||||
// is no request queued either.
|
// is no request queued either.
|
||||||
auto it = queue_.find(url);
|
auto it = queue_.find(url);
|
||||||
@ -289,6 +296,7 @@ public:
|
|||||||
response = desc.response_;
|
response = desc.response_;
|
||||||
request_timeout = desc.request_timeout_,
|
request_timeout = desc.request_timeout_,
|
||||||
callback = desc.callback_;
|
callback = desc.callback_;
|
||||||
|
connect_callback = desc.connect_callback_;
|
||||||
return (true);
|
return (true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -307,13 +315,16 @@ public:
|
|||||||
/// stored.
|
/// stored.
|
||||||
/// @param request_timeout Requested timeout for the transaction in
|
/// @param request_timeout Requested timeout for the transaction in
|
||||||
/// milliseconds.
|
/// milliseconds.
|
||||||
/// @param callback Pointer to the user callback to be invoked when the
|
/// @param request_callback Pointer to the user callback to be invoked when the
|
||||||
/// transaction ends.
|
/// transaction ends.
|
||||||
|
/// @param connect_callback Pointer to the user callback to be invoked when the
|
||||||
|
/// client connects to the server.
|
||||||
void queueRequest(const Url& url,
|
void queueRequest(const Url& url,
|
||||||
const HttpRequestPtr& request,
|
const HttpRequestPtr& request,
|
||||||
const HttpResponsePtr& response,
|
const HttpResponsePtr& response,
|
||||||
const long request_timeout,
|
const long request_timeout,
|
||||||
const HttpClient::RequestHandler& callback) {
|
const HttpClient::RequestHandler& request_callback,
|
||||||
|
const HttpClient::ConnectHandler& connect_callback) {
|
||||||
auto it = conns_.find(url);
|
auto it = conns_.find(url);
|
||||||
if (it != conns_.end()) {
|
if (it != conns_.end()) {
|
||||||
ConnectionPtr conn = it->second;
|
ConnectionPtr conn = it->second;
|
||||||
@ -322,12 +333,13 @@ public:
|
|||||||
// Connection is busy, so let's queue the request.
|
// Connection is busy, so let's queue the request.
|
||||||
queue_[url].push(RequestDescriptor(request, response,
|
queue_[url].push(RequestDescriptor(request, response,
|
||||||
request_timeout,
|
request_timeout,
|
||||||
callback));
|
request_callback,
|
||||||
|
connect_callback));
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Connection is idle, so we can start the transaction.
|
// Connection is idle, so we can start the transaction.
|
||||||
conn->doTransaction(request, response, request_timeout,
|
conn->doTransaction(request, response, request_timeout,
|
||||||
callback);
|
request_callback, connect_callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@ -335,7 +347,8 @@ public:
|
|||||||
// it and start the transaction.
|
// it and start the transaction.
|
||||||
ConnectionPtr conn(new Connection(io_service_, shared_from_this(),
|
ConnectionPtr conn(new Connection(io_service_, shared_from_this(),
|
||||||
url));
|
url));
|
||||||
conn->doTransaction(request, response, request_timeout, callback);
|
conn->doTransaction(request, response, request_timeout, request_callback,
|
||||||
|
connect_callback);
|
||||||
conns_[url] = conn;
|
conns_[url] = conn;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -389,13 +402,17 @@ private:
|
|||||||
/// be stored.
|
/// be stored.
|
||||||
/// @param request_timeout Requested timeout for the transaction.
|
/// @param request_timeout Requested timeout for the transaction.
|
||||||
/// @param callback Pointer to the user callback.
|
/// @param callback Pointer to the user callback.
|
||||||
|
/// @param connect_callback pointer to the user callback to be invoked
|
||||||
|
/// when the client connects to the server.
|
||||||
RequestDescriptor(const HttpRequestPtr& request,
|
RequestDescriptor(const HttpRequestPtr& request,
|
||||||
const HttpResponsePtr& response,
|
const HttpResponsePtr& response,
|
||||||
const long request_timeout,
|
const long request_timeout,
|
||||||
const HttpClient::RequestHandler& callback)
|
const HttpClient::RequestHandler& callback,
|
||||||
|
const HttpClient::ConnectHandler& connect_callback)
|
||||||
: request_(request), response_(response),
|
: request_(request), response_(response),
|
||||||
request_timeout_(request_timeout),
|
request_timeout_(request_timeout),
|
||||||
callback_(callback) {
|
callback_(callback),
|
||||||
|
connect_callback_(connect_callback) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @brief Holds pointer to the request.
|
/// @brief Holds pointer to the request.
|
||||||
@ -406,6 +423,8 @@ private:
|
|||||||
long request_timeout_;
|
long request_timeout_;
|
||||||
/// @brief Holds pointer to the user callback.
|
/// @brief Holds pointer to the user callback.
|
||||||
HttpClient::RequestHandler callback_;
|
HttpClient::RequestHandler callback_;
|
||||||
|
/// @brief Holds pointer to the user callback for connect.
|
||||||
|
HttpClient::ConnectHandler connect_callback_;
|
||||||
};
|
};
|
||||||
|
|
||||||
/// @brief Holds the queue of requests for different URLs.
|
/// @brief Holds the queue of requests for different URLs.
|
||||||
@ -436,7 +455,8 @@ void
|
|||||||
Connection::doTransaction(const HttpRequestPtr& request,
|
Connection::doTransaction(const HttpRequestPtr& request,
|
||||||
const HttpResponsePtr& response,
|
const HttpResponsePtr& response,
|
||||||
const long request_timeout,
|
const long request_timeout,
|
||||||
const HttpClient::RequestHandler& callback) {
|
const HttpClient::RequestHandler& callback,
|
||||||
|
const HttpClient::ConnectHandler& connect_callback) {
|
||||||
try {
|
try {
|
||||||
current_request_ = request;
|
current_request_ = request;
|
||||||
current_response_ = response;
|
current_response_ = response;
|
||||||
@ -467,13 +487,16 @@ Connection::doTransaction(const HttpRequestPtr& request,
|
|||||||
.arg(HttpMessageParserBase::logFormatHttpMessage(request->toString(),
|
.arg(HttpMessageParserBase::logFormatHttpMessage(request->toString(),
|
||||||
MAX_LOGGED_MESSAGE_SIZE));
|
MAX_LOGGED_MESSAGE_SIZE));
|
||||||
|
|
||||||
|
// Setup request timer.
|
||||||
|
scheduleTimer(request_timeout);
|
||||||
|
|
||||||
/// @todo We're getting a hostname but in fact it is expected to be an IP address.
|
/// @todo We're getting a hostname but in fact it is expected to be an IP address.
|
||||||
/// We should extend the TCPEndpoint to also accept names. Currently, it will fall
|
/// We should extend the TCPEndpoint to also accept names. Currently, it will fall
|
||||||
/// over for names.
|
/// over for names.
|
||||||
TCPEndpoint endpoint(url_.getStrippedHostname(),
|
TCPEndpoint endpoint(url_.getStrippedHostname(),
|
||||||
static_cast<unsigned short>(url_.getPort()));
|
static_cast<unsigned short>(url_.getPort()));
|
||||||
SocketCallback socket_cb(boost::bind(&Connection::connectCallback, shared_from_this(),
|
SocketCallback socket_cb(boost::bind(&Connection::connectCallback, shared_from_this(),
|
||||||
request_timeout, _1));
|
connect_callback, _1));
|
||||||
|
|
||||||
// Establish new connection or use existing connection.
|
// Establish new connection or use existing connection.
|
||||||
socket_.open(&endpoint, socket_cb);
|
socket_.open(&endpoint, socket_cb);
|
||||||
@ -557,10 +580,11 @@ Connection::terminate(const boost::system::error_code& ec,
|
|||||||
HttpRequestPtr request;
|
HttpRequestPtr request;
|
||||||
long request_timeout;
|
long request_timeout;
|
||||||
HttpClient::RequestHandler callback;
|
HttpClient::RequestHandler callback;
|
||||||
|
HttpClient::ConnectHandler connect_callback;
|
||||||
ConnectionPoolPtr conn_pool = conn_pool_.lock();
|
ConnectionPoolPtr conn_pool = conn_pool_.lock();
|
||||||
if (conn_pool && conn_pool->getNextRequest(url_, request, response, request_timeout,
|
if (conn_pool && conn_pool->getNextRequest(url_, request, response, request_timeout,
|
||||||
callback)) {
|
callback, connect_callback)) {
|
||||||
doTransaction(request, response, request_timeout, callback);
|
doTransaction(request, response, request_timeout, callback, connect_callback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -599,7 +623,17 @@ Connection::doReceive() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
Connection::connectCallback(const long request_timeout, const boost::system::error_code& ec) {
|
Connection::connectCallback(HttpClient::ConnectHandler connect_callback,
|
||||||
|
const boost::system::error_code& ec) {
|
||||||
|
// Run user defined connect callback if specified.
|
||||||
|
if (connect_callback) {
|
||||||
|
// If the user defined callback indicates that the connection
|
||||||
|
// should not be continued.
|
||||||
|
if (!connect_callback(ec)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// In some cases the "in progress" status code may be returned. It doesn't
|
// In some cases the "in progress" status code may be returned. It doesn't
|
||||||
// indicate an error. Sending the request over the socket is expected to
|
// indicate an error. Sending the request over the socket is expected to
|
||||||
// be successful. Getting such status appears to be highly dependent on
|
// be successful. Getting such status appears to be highly dependent on
|
||||||
@ -610,9 +644,6 @@ Connection::connectCallback(const long request_timeout, const boost::system::err
|
|||||||
terminate(ec);
|
terminate(ec);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Setup request timer.
|
|
||||||
scheduleTimer(request_timeout);
|
|
||||||
|
|
||||||
// Start sending the request asynchronously.
|
// Start sending the request asynchronously.
|
||||||
doSend();
|
doSend();
|
||||||
}
|
}
|
||||||
@ -736,8 +767,9 @@ HttpClient::HttpClient(IOService& io_service)
|
|||||||
void
|
void
|
||||||
HttpClient::asyncSendRequest(const Url& url, const HttpRequestPtr& request,
|
HttpClient::asyncSendRequest(const Url& url, const HttpRequestPtr& request,
|
||||||
const HttpResponsePtr& response,
|
const HttpResponsePtr& response,
|
||||||
const HttpClient::RequestHandler& callback,
|
const HttpClient::RequestHandler& request_callback,
|
||||||
const HttpClient::RequestTimeout& request_timeout) {
|
const HttpClient::RequestTimeout& request_timeout,
|
||||||
|
const HttpClient::ConnectHandler& connect_callback) {
|
||||||
if (!url.isValid()) {
|
if (!url.isValid()) {
|
||||||
isc_throw(HttpClientError, "invalid URL specified for the HTTP client");
|
isc_throw(HttpClientError, "invalid URL specified for the HTTP client");
|
||||||
}
|
}
|
||||||
@ -750,12 +782,12 @@ HttpClient::asyncSendRequest(const Url& url, const HttpRequestPtr& request,
|
|||||||
isc_throw(HttpClientError, "HTTP response must not be null");
|
isc_throw(HttpClientError, "HTTP response must not be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!callback) {
|
if (!request_callback) {
|
||||||
isc_throw(HttpClientError, "callback for HTTP transaction must not be null");
|
isc_throw(HttpClientError, "callback for HTTP transaction must not be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_->conn_pool_->queueRequest(url, request, response, request_timeout.value_,
|
impl_->conn_pool_->queueRequest(url, request, response, request_timeout.value_,
|
||||||
callback);
|
request_callback, connect_callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
@ -82,6 +82,12 @@ public:
|
|||||||
const HttpResponsePtr&,
|
const HttpResponsePtr&,
|
||||||
const std::string&)> RequestHandler;
|
const std::string&)> RequestHandler;
|
||||||
|
|
||||||
|
/// @brief Optional handler invoked when client connects to the server.
|
||||||
|
///
|
||||||
|
/// Returned boolean value indicates whether the client should continue
|
||||||
|
/// connecting to the server (if true) or not (false).
|
||||||
|
typedef std::function<bool(const boost::system::error_code&)> ConnectHandler;
|
||||||
|
|
||||||
/// @brief Constructor.
|
/// @brief Constructor.
|
||||||
///
|
///
|
||||||
/// @param io_service IO service to be used by the HTTP client.
|
/// @param io_service IO service to be used by the HTTP client.
|
||||||
@ -141,16 +147,21 @@ public:
|
|||||||
/// @param url URL where the request should be send.
|
/// @param url URL where the request should be send.
|
||||||
/// @param request Pointer to the object holding a request.
|
/// @param request Pointer to the object holding a request.
|
||||||
/// @param response Pointer to the object where response should be stored.
|
/// @param response Pointer to the object where response should be stored.
|
||||||
/// @param callback Pointer to the user callback function.
|
/// @param request_callback Pointer to the user callback function invoked
|
||||||
|
/// when transaction ends.
|
||||||
/// @param request_timeout Timeout for the transaction in milliseconds.
|
/// @param request_timeout Timeout for the transaction in milliseconds.
|
||||||
|
/// @param connect_callback Optional callback invoked when the client
|
||||||
|
/// connects to the server.
|
||||||
///
|
///
|
||||||
/// @throw HttpClientError If invalid arguments were provided.
|
/// @throw HttpClientError If invalid arguments were provided.
|
||||||
void asyncSendRequest(const Url& url,
|
void asyncSendRequest(const Url& url,
|
||||||
const HttpRequestPtr& request,
|
const HttpRequestPtr& request,
|
||||||
const HttpResponsePtr& response,
|
const HttpResponsePtr& response,
|
||||||
const RequestHandler& callback,
|
const RequestHandler& request_callback,
|
||||||
const RequestTimeout& request_timeout =
|
const RequestTimeout& request_timeout =
|
||||||
RequestTimeout(10000));
|
RequestTimeout(10000),
|
||||||
|
const ConnectHandler& connect_callback =
|
||||||
|
ConnectHandler());
|
||||||
|
|
||||||
/// @brief Closes all connections.
|
/// @brief Closes all connections.
|
||||||
void stop();
|
void stop();
|
||||||
|
@ -1249,5 +1249,58 @@ TEST_F(HttpClientTest, clientRequestTimeout) {
|
|||||||
ASSERT_NO_THROW(runIOService());
|
ASSERT_NO_THROW(runIOService());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test that client times out when connection takes too long.
|
||||||
|
TEST_F(HttpClientTest, clientConnectTimeout) {
|
||||||
|
// Start the server.
|
||||||
|
ASSERT_NO_THROW(listener_.start());
|
||||||
|
|
||||||
|
// Create the client.
|
||||||
|
HttpClient client(io_service_);
|
||||||
|
|
||||||
|
// Specify the URL of the server.
|
||||||
|
Url url("http://127.0.0.1:18123");
|
||||||
|
|
||||||
|
unsigned cb_num = 0;
|
||||||
|
|
||||||
|
PostHttpRequestJsonPtr request = createRequest("sequence", 1);
|
||||||
|
HttpResponseJsonPtr response(new HttpResponseJson());
|
||||||
|
ASSERT_NO_THROW(client.asyncSendRequest(url, request, response,
|
||||||
|
[this, &cb_num](const boost::system::error_code& ec,
|
||||||
|
const HttpResponsePtr& response,
|
||||||
|
const std::string&) {
|
||||||
|
if (++cb_num > 1) {
|
||||||
|
io_service_.stop();
|
||||||
|
}
|
||||||
|
// In this particular case we know exactly the type of the
|
||||||
|
// IO error returned, because the client explicitly sets this
|
||||||
|
// error code.
|
||||||
|
EXPECT_TRUE(ec.value() == boost::asio::error::timed_out);
|
||||||
|
// There should be no response returned.
|
||||||
|
EXPECT_FALSE(response);
|
||||||
|
|
||||||
|
}, HttpClient::RequestTimeout(100),
|
||||||
|
|
||||||
|
// This callback is invoked upon an attempt to connect to the
|
||||||
|
// server. The false value indicates to the HttpClient to not
|
||||||
|
// try to send a request to the server. This simulates the
|
||||||
|
// case of connect() taking very long and should eventually
|
||||||
|
// cause the transaction to time out.
|
||||||
|
[](const boost::system::error_code& ec) {
|
||||||
|
return (false);
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Create another request after the timeout. It should be handled ok.
|
||||||
|
ASSERT_NO_THROW(client.asyncSendRequest(url, request, response,
|
||||||
|
[this, &cb_num](const boost::system::error_code& /*ec*/, const HttpResponsePtr&,
|
||||||
|
const std::string&) {
|
||||||
|
if (++cb_num > 1) {
|
||||||
|
io_service_.stop();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Actually trigger the requests.
|
||||||
|
ASSERT_NO_THROW(runIOService());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user