fix: Add sending queue to ng web server (#2273)

This commit is contained in:
Sergey Kuznetsov
2025-07-09 17:29:57 +01:00
committed by GitHub
parent 413b823976
commit bfe5b52a64
13 changed files with 401 additions and 85 deletions

View File

@@ -18,6 +18,7 @@
//==============================================================================
#include "util/AsioContextTestFixture.hpp"
#include "util/CoroutineGroup.hpp"
#include "util/Taggable.hpp"
#include "util/TestHttpClient.hpp"
#include "util/TestHttpServer.hpp"
@@ -42,8 +43,10 @@
#include <chrono>
#include <cstddef>
#include <memory>
#include <optional>
#include <ranges>
#include <string>
#include <utility>
using namespace web::ng::impl;
@@ -52,16 +55,16 @@ using namespace util::config;
namespace http = boost::beast::http;
struct HttpConnectionTests : SyncAsioContextTest {
PlainHttpConnection
std::unique_ptr<PlainHttpConnection>
acceptConnection(boost::asio::yield_context yield)
{
auto expectedSocket = httpServer_.accept(yield);
[&]() { ASSERT_TRUE(expectedSocket.has_value()) << expectedSocket.error().message(); }();
auto ip = expectedSocket->remote_endpoint().address().to_string();
PlainHttpConnection connection{
auto connection = std::make_unique<PlainHttpConnection>(
std::move(expectedSocket).value(), std::move(ip), boost::beast::flat_buffer{}, tagDecoratorFactory_
};
connection.setTimeout(std::chrono::milliseconds{100});
);
connection->setTimeout(std::chrono::milliseconds{100});
return connection;
}
@@ -83,7 +86,7 @@ TEST_F(HttpConnectionTests, wasUpgraded)
runSpawn([this](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
EXPECT_FALSE(connection.wasUpgraded());
EXPECT_FALSE(connection->wasUpgraded());
});
}
@@ -102,7 +105,7 @@ TEST_F(HttpConnectionTests, Receive)
runSpawn([this](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
auto expectedRequest = connection.receive(yield);
auto expectedRequest = connection->receive(yield);
ASSERT_TRUE(expectedRequest.has_value()) << expectedRequest.error().message();
ASSERT_TRUE(expectedRequest->isHttp());
@@ -126,8 +129,8 @@ TEST_F(HttpConnectionTests, ReceiveTimeout)
runSpawn([this](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
connection.setTimeout(std::chrono::milliseconds{1});
auto expectedRequest = connection.receive(yield);
connection->setTimeout(std::chrono::milliseconds{1});
auto expectedRequest = connection->receive(yield);
EXPECT_FALSE(expectedRequest.has_value());
});
}
@@ -142,8 +145,8 @@ TEST_F(HttpConnectionTests, ReceiveClientDisconnected)
runSpawn([this](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
connection.setTimeout(std::chrono::milliseconds{1});
auto expectedRequest = connection.receive(yield);
connection->setTimeout(std::chrono::milliseconds{1});
auto expectedRequest = connection->receive(yield);
EXPECT_FALSE(expectedRequest.has_value());
});
}
@@ -170,7 +173,7 @@ TEST_F(HttpConnectionTests, Send)
runSpawn([this, &response](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
auto maybeError = connection.send(response, yield);
auto maybeError = connection->send(response, yield);
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
});
}
@@ -186,7 +189,7 @@ TEST_F(HttpConnectionTests, SendMultipleTimes)
for ([[maybe_unused]] auto i : std::ranges::iota_view{0, 3}) {
auto const expectedResponse = httpClient_.receive(yield, std::chrono::milliseconds{100});
[&]() { ASSERT_TRUE(expectedResponse.has_value()) << maybeError->message(); }();
[&]() { ASSERT_TRUE(expectedResponse.has_value()) << expectedResponse.error().message(); }();
auto const receivedResponse = expectedResponse.value();
auto const sentResponse = Response{response}.intoHttpResponse();
@@ -201,12 +204,77 @@ TEST_F(HttpConnectionTests, SendMultipleTimes)
auto connection = acceptConnection(yield);
for ([[maybe_unused]] auto i : std::ranges::iota_view{0, 3}) {
auto maybeError = connection.send(response, yield);
auto maybeError = connection->send(response, yield);
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
}
});
}
TEST_F(HttpConnectionTests, SendMultipleTimesFromMultipleCoroutines)
{
Request const request{request_};
Response const response{http::status::ok, "some response data", request};
boost::asio::spawn(ctx_, [this, response = response](boost::asio::yield_context yield) mutable {
auto const maybeError =
httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
for ([[maybe_unused]] auto i : std::ranges::iota_view{0, 3}) {
auto const expectedResponse = httpClient_.receive(yield, std::chrono::milliseconds{100});
[&]() { ASSERT_TRUE(expectedResponse.has_value()) << expectedResponse.error().message(); }();
auto const receivedResponse = expectedResponse.value();
auto const sentResponse = Response{response}.intoHttpResponse();
EXPECT_EQ(receivedResponse.result(), sentResponse.result());
EXPECT_EQ(receivedResponse.body(), sentResponse.body());
EXPECT_EQ(receivedResponse.version(), request_.version());
EXPECT_TRUE(receivedResponse.keep_alive());
}
});
runSpawn([this, &response](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
util::CoroutineGroup group{yield};
for ([[maybe_unused]] auto i : std::ranges::iota_view{0, 3}) {
group.spawn(yield, [&response, &connection](boost::asio::yield_context innerYield) {
auto const maybeError = connection->send(response, innerYield);
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
});
}
group.asyncWait(yield);
});
}
TEST_F(HttpConnectionTests, SendMultipleTimesClientDisconnected)
{
Response const response{http::status::ok, "some response data", Request{request_}};
boost::asio::spawn(ctx_, [this, response = response](boost::asio::yield_context yield) mutable {
auto const maybeError =
httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{1});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
auto const expectedResponse = httpClient_.receive(yield, std::chrono::milliseconds{100});
[&]() { ASSERT_TRUE(expectedResponse.has_value()) << expectedResponse.error().message(); }();
httpClient_.disconnect();
});
runSpawn([this, &response](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
connection->setTimeout(std::chrono::milliseconds{1});
auto maybeError = connection->send(response, yield);
size_t counter{1};
while (not maybeError.has_value() and counter < 100) {
++counter;
maybeError = connection->send(response, yield);
}
// Sending after getting an error should be safe
maybeError = connection->send(response, yield);
EXPECT_TRUE(maybeError.has_value());
EXPECT_LT(counter, 100);
});
}
TEST_F(HttpConnectionTests, SendClientDisconnected)
{
Response const response{http::status::ok, "some response data", Request{request_}};
@@ -217,12 +285,12 @@ TEST_F(HttpConnectionTests, SendClientDisconnected)
});
runSpawn([this, &response](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
connection.setTimeout(std::chrono::milliseconds{1});
auto maybeError = connection.send(response, yield);
connection->setTimeout(std::chrono::milliseconds{1});
auto maybeError = connection->send(response, yield);
size_t counter{1};
while (not maybeError.has_value() and counter < 100) {
++counter;
maybeError = connection.send(response, yield);
maybeError = connection->send(response, yield);
}
EXPECT_TRUE(maybeError.has_value());
EXPECT_LT(counter, 100);
@@ -246,8 +314,8 @@ TEST_F(HttpConnectionTests, Close)
runSpawn([this](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
connection.setTimeout(std::chrono::milliseconds{1});
connection.close(yield);
connection->setTimeout(std::chrono::milliseconds{1});
connection->close(yield);
});
}
@@ -263,7 +331,7 @@ TEST_F(HttpConnectionTests, IsUpgradeRequested_GotHttpRequest)
runSpawn([this](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
auto result = connection.isUpgradeRequested(yield);
auto result = connection->isUpgradeRequested(yield);
[&]() { ASSERT_TRUE(result.has_value()) << result.error().message(); }();
EXPECT_FALSE(result.value());
});
@@ -278,8 +346,8 @@ TEST_F(HttpConnectionTests, IsUpgradeRequested_FailedToFetch)
runSpawn([this](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
connection.setTimeout(std::chrono::milliseconds{1});
auto result = connection.isUpgradeRequested(yield);
connection->setTimeout(std::chrono::milliseconds{1});
auto result = connection->isUpgradeRequested(yield);
EXPECT_FALSE(result.has_value());
});
}
@@ -295,11 +363,11 @@ TEST_F(HttpConnectionTests, Upgrade)
runSpawn([this](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
auto const expectedResult = connection.isUpgradeRequested(yield);
auto const expectedResult = connection->isUpgradeRequested(yield);
[&]() { ASSERT_TRUE(expectedResult.has_value()) << expectedResult.error().message(); }();
[&]() { ASSERT_TRUE(expectedResult.value()); }();
auto expectedWsConnection = connection.upgrade(tagDecoratorFactory_, yield);
auto expectedWsConnection = connection->upgrade(tagDecoratorFactory_, yield);
[&]() { ASSERT_TRUE(expectedWsConnection.has_value()) << expectedWsConnection.error().message(); }();
});
}
@@ -313,7 +381,7 @@ TEST_F(HttpConnectionTests, Ip)
runSpawn([this](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
EXPECT_TRUE(connection.ip() == "127.0.0.1" or connection.ip() == "::1") << connection.ip();
EXPECT_TRUE(connection->ip() == "127.0.0.1" or connection->ip() == "::1") << connection->ip();
});
}
@@ -329,13 +397,13 @@ TEST_F(HttpConnectionTests, isAdminSetAdmin)
runSpawn([&](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
EXPECT_FALSE(connection.isAdmin());
EXPECT_FALSE(connection->isAdmin());
connection.setIsAdmin(adminSetter.AsStdFunction());
EXPECT_TRUE(connection.isAdmin());
connection->setIsAdmin(adminSetter.AsStdFunction());
EXPECT_TRUE(connection->isAdmin());
// Setter shouldn't not be called here because isAdmin is already set
connection.setIsAdmin(adminSetter.AsStdFunction());
EXPECT_TRUE(connection.isAdmin());
connection->setIsAdmin(adminSetter.AsStdFunction());
EXPECT_TRUE(connection->isAdmin());
});
}