Skip to content

Commit

Permalink
refactor: Coroutine based webserver (#1699)
Browse files Browse the repository at this point in the history
Code of new coroutine-based web server. The new server is not connected
to Clio and not ready to use yet.
For #919.
  • Loading branch information
kuznetsss authored and godexsoft committed Nov 11, 2024
1 parent 5c77e59 commit b8f1deb
Show file tree
Hide file tree
Showing 58 changed files with 5,581 additions and 488 deletions.
12 changes: 0 additions & 12 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,6 @@ jobs:
name: clio_tests_${{ runner.os }}_${{ matrix.build_type }}_${{ steps.conan.outputs.conan_profile }}
path: build/clio_*tests

- name: Upload test data
if: ${{ !matrix.code_coverage }}
uses: actions/upload-artifact@v4
with:
name: clio_test_data_${{ runner.os }}_${{ matrix.build_type }}_${{ steps.conan.outputs.conan_profile }}
path: build/tests/unit/test_data

- name: Save cache
uses: ./.github/actions/save_cache
with:
Expand Down Expand Up @@ -219,11 +212,6 @@ jobs:
with:
name: clio_tests_${{ runner.os }}_${{ matrix.build_type }}_${{ matrix.conan_profile }}

- uses: actions/download-artifact@v4
with:
name: clio_test_data_${{ runner.os }}_${{ matrix.build_type }}_${{ matrix.conan_profile }}
path: tests/unit/test_data

- name: Run clio_tests
run: |
chmod +x ./clio_tests
Expand Down
9 changes: 8 additions & 1 deletion docs/examples/config/example-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,14 @@
"admin_password": "xrp",
// If local_admin is true, Clio will consider requests come from 127.0.0.1 as admin requests
// It's true by default unless admin_password is set,'local_admin' : true and 'admin_password' can not be set at the same time
"local_admin": false
"local_admin": false,
"processing_policy": "parallel", // Could be "sequent" or "parallel".
// For sequent policy request from one client connection will be processed one by one and the next one will not be read before
// the previous one is processed. For parallel policy Clio will take all requests and process them in parallel and
// send a reply for each request whenever it is ready.
"parallel_requests_limit": 10 // Optional parameter, used only if "processing_strategy" is "parallel".
It limits the number of requests for one client connection processed in parallel. Infinite if not specified.

},
// Time in seconds for graceful shutdown. Defaults to 10 seconds. Not fully implemented yet.
"graceful_period": 10.0,
Expand Down
1 change: 1 addition & 0 deletions src/util/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ target_sources(
clio_util
PRIVATE build/Build.cpp
config/Config.cpp
CoroutineGroup.cpp
log/Logger.cpp
prometheus/Http.cpp
prometheus/Label.cpp
Expand Down
76 changes: 76 additions & 0 deletions src/util/CoroutineGroup.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================

#include "util/CoroutineGroup.hpp"

#include "util/Assert.hpp"

#include <boost/asio/spawn.hpp>
#include <boost/asio/steady_timer.hpp>

#include <cstddef>
#include <functional>
#include <optional>
#include <utility>

namespace util {

CoroutineGroup::CoroutineGroup(boost::asio::yield_context yield, std::optional<int> maxChildren)
: timer_{yield.get_executor(), boost::asio::steady_timer::duration::max()}, maxChildren_{maxChildren}
{
}

CoroutineGroup::~CoroutineGroup()
{
ASSERT(childrenCounter_ == 0, "CoroutineGroup is destroyed without waiting for child coroutines to finish");
}

bool
CoroutineGroup::spawn(boost::asio::yield_context yield, std::function<void(boost::asio::yield_context)> fn)
{
if (maxChildren_.has_value() && childrenCounter_ >= *maxChildren_)
return false;

++childrenCounter_;
boost::asio::spawn(yield, [this, fn = std::move(fn)](boost::asio::yield_context yield) {
fn(yield);
--childrenCounter_;
if (childrenCounter_ == 0)
timer_.cancel();
});
return true;
}

void
CoroutineGroup::asyncWait(boost::asio::yield_context yield)
{
if (childrenCounter_ == 0)
return;

boost::system::error_code error;
timer_.async_wait(yield[error]);
}

size_t
CoroutineGroup::size() const
{
return childrenCounter_;
}

} // namespace util
88 changes: 88 additions & 0 deletions src/util/CoroutineGroup.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================

#pragma once

#include <boost/asio/spawn.hpp>
#include <boost/asio/steady_timer.hpp>

#include <cstddef>
#include <functional>
#include <optional>

namespace util {

/**
* @brief CoroutineGroup is a helper class to manage a group of coroutines. It allows to spawn multiple coroutines and
* wait for all of them to finish.
*/
class CoroutineGroup {
boost::asio::steady_timer timer_;
std::optional<int> maxChildren_;
int childrenCounter_{0};

public:
/**
* @brief Construct a new Coroutine Group object
*
* @param yield The yield context to use for the internal timer
* @param maxChildren The maximum number of coroutines that can be spawned at the same time. If not provided, there
* is no limit
*/
CoroutineGroup(boost::asio::yield_context yield, std::optional<int> maxChildren = std::nullopt);

/**
* @brief Destroy the Coroutine Group object
*
* @note asyncWait() must be called before the object is destroyed
*/
~CoroutineGroup();

/**
* @brief Spawn a new coroutine in the group
*
* @param yield The yield context to use for the coroutine (it should be the same as the one used in the
* constructor)
* @param fn The function to execute
* @return true If the coroutine was spawned successfully. false if the maximum number of coroutines has been
* reached
*/
bool
spawn(boost::asio::yield_context yield, std::function<void(boost::asio::yield_context)> fn);

/**
* @brief Wait for all the coroutines in the group to finish
*
* @note This method must be called before the object is destroyed
*
* @param yield The yield context to use for the internal timer
*/
void
asyncWait(boost::asio::yield_context yield);

/**
* @brief Get the number of coroutines in the group
*
* @return size_t The number of coroutines in the group
*/
size_t
size() const;
};

} // namespace util
71 changes: 71 additions & 0 deletions src/util/WithTimeout.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================

#pragma once

#include <boost/asio/associated_executor.hpp>
#include <boost/asio/bind_cancellation_slot.hpp>
#include <boost/asio/cancellation_signal.hpp>
#include <boost/asio/cancellation_type.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/system/detail/error_code.hpp>
#include <boost/system/errc.hpp>

#include <chrono>
#include <ctime>
#include <memory>

namespace util {

/**
* @brief Perform a coroutine operation with a timeout.
*
* @tparam Operation The operation type to perform. Must be a callable accepting yield context with bound cancellation
* token.
* @param operation The operation to perform.
* @param yield The yield context.
* @param timeout The timeout duration.
* @return The error code of the operation.
*/
template <typename Operation>
boost::system::error_code
withTimeout(Operation&& operation, boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout)
{
boost::system::error_code error;
auto operationCompleted = std::make_shared<bool>(false);
boost::asio::cancellation_signal cancellationSignal;
auto cyield = boost::asio::bind_cancellation_slot(cancellationSignal.slot(), yield[error]);

boost::asio::steady_timer timer{boost::asio::get_associated_executor(cyield), timeout};
timer.async_wait([&cancellationSignal, operationCompleted](boost::system::error_code errorCode) {
if (!errorCode and !*operationCompleted)
cancellationSignal.emit(boost::asio::cancellation_type::terminal);
});
operation(cyield);
*operationCompleted = true;

// Map error code to timeout
if (error == boost::system::errc::operation_canceled) {
return boost::system::errc::make_error_code(boost::system::errc::timed_out);
}
return error;
}

} // namespace util
43 changes: 5 additions & 38 deletions src/util/requests/impl/WsConnectionImpl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

#pragma once

#include "util/WithTimeout.hpp"
#include "util/requests/Types.hpp"
#include "util/requests/WsConnection.hpp"

Expand Down Expand Up @@ -67,15 +68,13 @@ class WsConnectionImpl : public WsConnection {

auto operation = [&](auto&& token) { ws_.async_read(buffer, token); };
if (timeout) {
withTimeout(operation, yield[errorCode], *timeout);
errorCode = util::withTimeout(operation, yield[errorCode], *timeout);
} else {
operation(yield[errorCode]);
}

if (errorCode) {
errorCode = mapError(errorCode);
if (errorCode)
return std::unexpected{RequestError{"Read error", errorCode}};
}

return boost::beast::buffers_to_string(std::move(buffer).data());
}
Expand All @@ -90,15 +89,13 @@ class WsConnectionImpl : public WsConnection {
boost::beast::error_code errorCode;
auto operation = [&](auto&& token) { ws_.async_write(boost::asio::buffer(message), token); };
if (timeout) {
withTimeout(operation, yield[errorCode], *timeout);
errorCode = util::withTimeout(operation, yield, *timeout);
} else {
operation(yield[errorCode]);
}

if (errorCode) {
errorCode = mapError(errorCode);
if (errorCode)
return RequestError{"Write error", errorCode};
}

return std::nullopt;
}
Expand All @@ -119,36 +116,6 @@ class WsConnectionImpl : public WsConnection {
return RequestError{"Close error", errorCode};
return std::nullopt;
}

private:
template <typename Operation>
static void
withTimeout(Operation&& operation, boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout)
{
auto isCompleted = std::make_shared<bool>(false);
boost::asio::cancellation_signal cancellationSignal;
auto cyield = boost::asio::bind_cancellation_slot(cancellationSignal.slot(), yield);

boost::asio::steady_timer timer{boost::asio::get_associated_executor(cyield), timeout};

// The timer below can be called with no error code even if the operation is completed before the timeout, so we
// need an additional flag here
timer.async_wait([&cancellationSignal, isCompleted](boost::system::error_code errorCode) {
if (!errorCode and !*isCompleted)
cancellationSignal.emit(boost::asio::cancellation_type::terminal);
});
operation(cyield);
*isCompleted = true;
}

static boost::system::error_code
mapError(boost::system::error_code const ec)
{
if (ec == boost::system::errc::operation_canceled) {
return boost::system::errc::make_error_code(boost::system::errc::timed_out);
}
return ec;
}
};

using PlainWsConnection = WsConnectionImpl<boost::beast::websocket::stream<boost::beast::tcp_stream>>;
Expand Down
8 changes: 6 additions & 2 deletions src/web/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ add_library(clio_web)
target_sources(
clio_web
PRIVATE Resolver.cpp
Server.cpp
dosguard/DOSGuard.cpp
dosguard/IntervalSweepHandler.cpp
dosguard/WhitelistHandler.cpp
impl/AdminVerificationStrategy.cpp
impl/ServerSslContext.cpp
ng/Connection.cpp
ng/impl/ConnectionHandler.cpp
ng/impl/ServerSslContext.cpp
ng/impl/WsConnection.cpp
ng/Server.cpp
ng/Request.cpp
ng/Response.cpp
)

target_link_libraries(clio_web PUBLIC clio_util)
Loading

0 comments on commit b8f1deb

Please sign in to comment.