From b79017a2e40dc5b74bfb9a290141c455bbc22021 Mon Sep 17 00:00:00 2001 From: Sungsik Date: Wed, 10 Sep 2025 14:19:37 +0200 Subject: [PATCH 01/35] Interleave writing and read data from storages --- src/proxy/cache/asio.h | 30 ++++++++++-- src/proxy/cache/disk/body.h | 66 +++++++++++++++++++++------ src/proxy/cache/disk/manager.h | 7 +-- src/proxy/handler.cpp | 37 +++++---------- test/unit/test_disk_cache_body.cpp | 2 +- test/unit/test_disk_cache_manager.cpp | 11 ++--- 6 files changed, 100 insertions(+), 53 deletions(-) diff --git a/src/proxy/cache/asio.h b/src/proxy/cache/asio.h index fb3167dc2..6685f8736 100644 --- a/src/proxy/cache/asio.h +++ b/src/proxy/cache/asio.h @@ -1,10 +1,13 @@ #pragma once +#include #include #include #include #include +using namespace boost::asio::experimental::awaitable_operators; + namespace uh::cluster::proxy::cache { template @@ -79,11 +82,28 @@ template coro async_write(ep::http::stream& s, T& t) { } }(); - while (true) { - std::span data = co_await writer.get(); - if (data.empty()) - break; - co_await s.write(data); + if constexpr (T::support_double_buffer::value) { + std::span data; + while (true) { + if (data.empty()) { + data = co_await writer.get(); + } else { + auto [d, _] = co_await (writer.get() && s.write(data)); + if (d.empty()) { + co_await s.write(data); + break; + } else { + data = d; + } + } + } + } else { + while (true) { + std::span data = co_await writer.get(); + if (data.empty()) + break; + co_await s.write(data); + } } } diff --git a/src/proxy/cache/disk/body.h b/src/proxy/cache/disk/body.h index 2dc14092b..892f8cb81 100644 --- a/src/proxy/cache/disk/body.h +++ b/src/proxy/cache/disk/body.h @@ -50,6 +50,8 @@ class reader_body { class writer_body { public: + using support_double_buffer = std::false_type; + writer_body(storage::data_view& storage, std::shared_ptr objh, std::size_t buffer_size = 32 * MEBI_BYTE) @@ -57,20 +59,37 @@ class writer_body { m_objh{std::move(objh)}, m_buffer(buffer_size) {} - coro> get() { + writer_body(const writer_body&) = delete; + writer_body& operator=(const writer_body&) = delete; + writer_body(writer_body&&) = delete; + writer_body& operator=(writer_body&&) = delete; + + coro> get() { co_return co_await _get(&m_buffer); } + +private: + storage::data_view& m_storage; + std::shared_ptr m_objh; + + std::size_t m_addr_index{0}; + std::size_t m_frag_offset{0}; + +protected: + std::vector m_buffer; + + coro> _get(std::vector* buffer) { std::size_t read_size = 0; address partial_addr; while (m_addr_index < m_objh->get_address().size() && - read_size < m_buffer.size()) { + read_size < buffer->size()) { auto frag = m_objh->get_address().get(m_addr_index); if (m_frag_offset > 0) { frag.pointer += m_frag_offset; frag.size -= m_frag_offset; } - if (frag.size + read_size > m_buffer.size()) { - auto remains = m_buffer.size() - read_size; + if (frag.size + read_size > buffer->size()) { + auto remains = buffer->size() - read_size; m_frag_offset += remains; frag.size = remains; partial_addr.push(frag); @@ -85,19 +104,40 @@ class writer_body { if (read_size > 0) { co_await m_storage.read_address(partial_addr, - {m_buffer.data(), read_size}); + {buffer->data(), read_size}); } - co_return std::span{m_buffer.data(), read_size}; + co_return std::span{buffer->data(), read_size}; } +}; -private: - storage::data_view& m_storage; - std::shared_ptr m_objh; +class double_buffered_writer_body : private writer_body { +public: + using support_double_buffer = std::true_type; + + double_buffered_writer_body(storage::data_view& storage, + std::shared_ptr objh, + std::size_t buffer_size = 32 * MEBI_BYTE) + : writer_body(storage, objh, buffer_size), + m_buffer2(buffer_size), + m_active(&m_buffer), + m_standby(&m_buffer2) {} + + double_buffered_writer_body(const double_buffered_writer_body&) = delete; + double_buffered_writer_body& + operator=(const double_buffered_writer_body&) = delete; + double_buffered_writer_body(double_buffered_writer_body&&) = delete; + double_buffered_writer_body& + operator=(double_buffered_writer_body&&) = delete; - std::vector m_buffer; + coro> get() { + auto rv = co_await writer_body::_get(m_active); + std::swap(m_active, m_standby); + co_return rv; + } - std::size_t m_addr_index = 0; - std::size_t m_frag_offset = 0; +private: + std::vector m_buffer2; + std::vector* m_active; + std::vector* m_standby; }; - } // namespace uh::cluster::proxy::cache::disk diff --git a/src/proxy/cache/disk/manager.h b/src/proxy/cache/disk/manager.h index f7eb75ada..cd9215a91 100644 --- a/src/proxy/cache/disk/manager.h +++ b/src/proxy/cache/disk/manager.h @@ -66,12 +66,13 @@ class manager { std::cout << "Total size after put: " << m_current_size << std::endl; } - std::optional get(object_metadata key) { + std::unique_ptr get(object_metadata key) { auto entry = m_cache->get(key); if (!entry) { - return std::nullopt; + return nullptr; } - return writer_body{m_storage, std::move(entry)}; + return std::make_unique(m_storage, + std::move(entry)); } static manager create(boost::asio::io_context& ioc, data_view& storage, diff --git a/src/proxy/handler.cpp b/src/proxy/handler.cpp index 3534d37c0..5b9286a69 100644 --- a/src/proxy/handler.cpp +++ b/src/proxy/handler.cpp @@ -7,6 +7,7 @@ #include #include #include +#include using namespace uh::cluster::ep::http; @@ -15,9 +16,7 @@ namespace uh::cluster::proxy { handler::handler( std::unique_ptr factory, std::function()> sf, - storage::data_view& dv, - cache::disk::manager& mgr, - std::size_t buffer_size) + storage::data_view& dv, cache::disk::manager& mgr, std::size_t buffer_size) : m_factory(std::move(factory)), m_sf(std::move(sf)), m_dv(dv), @@ -53,7 +52,8 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { co_await m_factory->create(incoming, rawreq); if (get_object::can_handle(*req)) { - auto writer = m_mgr.get(cache::disk::object_metadata{ req->object_key() }); + auto writer = + m_mgr.get(cache::disk::object_metadata{req->object_key()}); if (writer) { LOG_INFO() << peer << ": handling from cache"; incoming.set_mode(forward_stream::deleting); @@ -71,12 +71,7 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { LOG_INFO() << peer << ": done reading complete request"; - std::span data = co_await writer->get(); - while (!data.empty()) { - LOG_INFO() << peer << ": sending " << data.size() << " bytes response"; - co_await incoming.write(data); - data = co_await writer->get(); - } + co_await cache::async_write(incoming, *writer); LOG_INFO() << peer << ": cache result served"; continue; @@ -116,7 +111,6 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { auto res = parser.release(); bs = outgoing.buffer_size(); - std::size_t read = 0ull; std::size_t len = std::stoul(res.at("Content-Length")); if (r.method() == boost::beast::http::verb::head && (res.result_int() / 100 == 2)) { @@ -124,27 +118,20 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { } LOG_INFO() << peer << ": sending response " << res.result_int() - << " " << res.reason() << " -- " << len; + << " " << res.reason() << " -- " << len; if (get_object::can_handle(*req)) { cache::disk::reader_body data(m_dv); - LOG_INFO() << peer << ": add " << buffer.size() << " response header"; + LOG_INFO() << peer << ": add " << buffer.size() + << " response header"; co_await data.put(buffer); - while (read < len) { - co_await outgoing.consume(); - - auto r = co_await outgoing.read(len - read); - LOG_INFO() << peer << ": add " << r.size() << " response data"; - co_await data.put(r); - - // r: data - read += r.size(); - } + co_await cache::async_read(outgoing, data, len); - co_await m_mgr.put(cache::disk::object_metadata{ req->object_key() }, data); - co_await outgoing.consume(); + co_await m_mgr.put( + cache::disk::object_metadata{req->object_key()}, data); } else { + std::size_t read = 0ull; while (read < len) { co_await outgoing.consume(); diff --git a/test/unit/test_disk_cache_body.cpp b/test/unit/test_disk_cache_body.cpp index c860e1764..520ccc11d 100644 --- a/test/unit/test_disk_cache_body.cpp +++ b/test/unit/test_disk_cache_body.cpp @@ -106,7 +106,7 @@ BOOST_AUTO_TEST_CASE(supports_write) { auto objh = std::make_shared(std::move(addr)); BOOST_TEST(objh->data_size() == data.size()); - writer_body body(data_view, std::move(objh), 16); + double_buffered_writer_body body(data_view, std::move(objh), 16); // Set up TCP sockets boost::asio::ip::tcp::acceptor acceptor(m_ioc, diff --git a/test/unit/test_disk_cache_manager.cpp b/test/unit/test_disk_cache_manager.cpp index 660710572..30b53ce9b 100644 --- a/test/unit/test_disk_cache_manager.cpp +++ b/test/unit/test_disk_cache_manager.cpp @@ -29,12 +29,11 @@ BOOST_AUTO_TEST_CASE(put_and_get_with_metadata) { boost::asio::co_spawn(m_ioc, mgr.put(key, rbody), boost::asio::use_future) .get(); - auto wbody_opt = mgr.get(key); - BOOST_TEST(wbody_opt.has_value()); + auto writer = mgr.get(key); + BOOST_TEST(writer != nullptr); - auto& wbody = wbody_opt.value(); auto buf = - boost::asio::co_spawn(m_ioc, wbody.get(), boost::asio::use_future) + boost::asio::co_spawn(m_ioc, writer->get(), boost::asio::use_future) .get(); BOOST_TEST(buf.size() == data.size()); @@ -67,8 +66,8 @@ BOOST_AUTO_TEST_CASE(eviction_test) { .get(); } - auto wbody_opt = mgr.get(keys.front()); - BOOST_TEST(!wbody_opt.has_value()); + auto writer = mgr.get(keys.front()); + BOOST_TEST(writer == nullptr); } BOOST_AUTO_TEST_SUITE_END() From 28c2ce604edefd58616ca3aadf83f727897367b8 Mon Sep 17 00:00:00 2001 From: Sungsik Date: Wed, 10 Sep 2025 14:54:41 +0200 Subject: [PATCH 02/35] Made it simpler --- src/proxy/cache/asio.h | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/src/proxy/cache/asio.h b/src/proxy/cache/asio.h index 6685f8736..06c36de37 100644 --- a/src/proxy/cache/asio.h +++ b/src/proxy/cache/asio.h @@ -41,8 +41,9 @@ template typename Body::writer make_writer(Body& b) { * * size can be replaced with parser implementation */ -template -coro async_read(ep::http::stream& s, T& t, std::size_t size) { +template +requires std::is_base_of_v +coro async_read(S& s, T& t, std::size_t size) { auto&& reader = [&]() -> auto&& { if constexpr (BodyType) { return make_reader(t); @@ -53,6 +54,7 @@ coro async_read(ep::http::stream& s, T& t, std::size_t size) { "T must satisfy BodyType or ReaderBodyType"); } }(); + while (size > 0) { auto sv = co_await s.read(size); if (sv.empty()) @@ -83,20 +85,11 @@ template coro async_write(ep::http::stream& s, T& t) { }(); if constexpr (T::support_double_buffer::value) { - std::span data; - while (true) { - if (data.empty()) { - data = co_await writer.get(); - } else { - auto [d, _] = co_await (writer.get() && s.write(data)); - if (d.empty()) { - co_await s.write(data); - break; - } else { - data = d; - } - } - } + std::span data = co_await writer.get(); + do { + auto [d, _] = co_await (writer.get() && s.write(data)); + data = d; + } while (!data.empty()); } else { while (true) { std::span data = co_await writer.get(); From 8c6bc2596db6b7935e9b09e06c33c0ffffd97ada Mon Sep 17 00:00:00 2001 From: Sungsik Date: Wed, 10 Sep 2025 16:06:49 +0200 Subject: [PATCH 03/35] nit --- src/proxy/handler.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/proxy/handler.cpp b/src/proxy/handler.cpp index 5b9286a69..96747f1c8 100644 --- a/src/proxy/handler.cpp +++ b/src/proxy/handler.cpp @@ -52,9 +52,9 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { co_await m_factory->create(incoming, rawreq); if (get_object::can_handle(*req)) { - auto writer = + auto wbody = m_mgr.get(cache::disk::object_metadata{req->object_key()}); - if (writer) { + if (wbody) { LOG_INFO() << peer << ": handling from cache"; incoming.set_mode(forward_stream::deleting); outgoing.set_mode(forward_stream::deleting); @@ -71,7 +71,7 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { LOG_INFO() << peer << ": done reading complete request"; - co_await cache::async_write(incoming, *writer); + co_await cache::async_write(incoming, *wbody); LOG_INFO() << peer << ": cache result served"; continue; @@ -121,22 +121,22 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { << " " << res.reason() << " -- " << len; if (get_object::can_handle(*req)) { - cache::disk::reader_body data(m_dv); LOG_INFO() << peer << ": add " << buffer.size() << " response header"; - co_await data.put(buffer); + cache::disk::reader_body rbody(m_dv); + co_await rbody.put(buffer); - co_await cache::async_read(outgoing, data, len); + co_await cache::async_read(outgoing, rbody, len); co_await m_mgr.put( - cache::disk::object_metadata{req->object_key()}, data); + cache::disk::object_metadata{req->object_key()}, rbody); } else { std::size_t read = 0ull; while (read < len) { co_await outgoing.consume(); auto r = co_await outgoing.read(len - read); - // r: data + // r: rbody read += r.size(); } From cfabdcdbc810da08688900ba42b0b765c4d30d2e Mon Sep 17 00:00:00 2001 From: Sungsik Date: Thu, 11 Sep 2025 07:35:34 +0200 Subject: [PATCH 04/35] fix wrong loop --- src/proxy/cache/asio.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/proxy/cache/asio.h b/src/proxy/cache/asio.h index 06c36de37..7d320beb2 100644 --- a/src/proxy/cache/asio.h +++ b/src/proxy/cache/asio.h @@ -85,11 +85,11 @@ template coro async_write(ep::http::stream& s, T& t) { }(); if constexpr (T::support_double_buffer::value) { - std::span data = co_await writer.get(); - do { + auto data = co_await writer.get(); + while (!data.empty()) { auto [d, _] = co_await (writer.get() && s.write(data)); data = d; - } while (!data.empty()); + } } else { while (true) { std::span data = co_await writer.get(); From aca79d66b9e18f70c9a7bcc0bedd5e155e171995 Mon Sep 17 00:00:00 2001 From: Sungsik Date: Thu, 11 Sep 2025 11:58:05 +0200 Subject: [PATCH 05/35] nit --- src/proxy/cache/asio.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/proxy/cache/asio.h b/src/proxy/cache/asio.h index 7d320beb2..2390748d1 100644 --- a/src/proxy/cache/asio.h +++ b/src/proxy/cache/asio.h @@ -85,14 +85,13 @@ template coro async_write(ep::http::stream& s, T& t) { }(); if constexpr (T::support_double_buffer::value) { - auto data = co_await writer.get(); - while (!data.empty()) { + for (auto data = co_await writer.get(); !data.empty();) { auto [d, _] = co_await (writer.get() && s.write(data)); data = d; } } else { while (true) { - std::span data = co_await writer.get(); + auto data = co_await writer.get(); if (data.empty()) break; co_await s.write(data); From 6b100beb730b87dede76ebdb2107fe0b9d4002b3 Mon Sep 17 00:00:00 2001 From: Sungsik Date: Tue, 16 Sep 2025 09:55:00 +0200 Subject: [PATCH 06/35] Use custom awaitable_operators --- src/proxy/cache/asio.h | 2 +- src/proxy/cache/awaitable_operators.h | 437 ++++++++++++++++++++++++++ 2 files changed, 438 insertions(+), 1 deletion(-) create mode 100644 src/proxy/cache/awaitable_operators.h diff --git a/src/proxy/cache/asio.h b/src/proxy/cache/asio.h index 2390748d1..ebc5beb50 100644 --- a/src/proxy/cache/asio.h +++ b/src/proxy/cache/asio.h @@ -1,10 +1,10 @@ #pragma once -#include #include #include #include #include +#include using namespace boost::asio::experimental::awaitable_operators; diff --git a/src/proxy/cache/awaitable_operators.h b/src/proxy/cache/awaitable_operators.h new file mode 100644 index 000000000..3884d9f6b --- /dev/null +++ b/src/proxy/cache/awaitable_operators.h @@ -0,0 +1,437 @@ +#pragma once + +#include +#include + +namespace boost { +namespace asio { +namespace experimental { +namespace awaitable_operators { +namespace detail { + +template +traced_awaitable +awaitable_wrap(traced_awaitable a, + constraint_t::value>* = 0) { + return a; +} + +template +traced_awaitable, Executor> +awaitable_wrap(traced_awaitable a, + constraint_t::value>* = 0) { + co_return std::optional(co_await std::move(a)); +} + +template +T& awaitable_unwrap(conditional_t& r, + constraint_t::value>* = 0) { + return r; +} + +template +T& awaitable_unwrap(std::optional>& r, + constraint_t::value>* = 0) { + return *r; +} + +} // namespace detail + +/// Wait for both operations to succeed. +/** + * If one operations fails, the other is cancelled as the AND-condition can no + * longer be satisfied. + */ +template +traced_awaitable +operator&&(traced_awaitable t, + traced_awaitable u) { + auto ex = co_await this_coro::executor; + + auto [order, ex0, ex1] = + co_await make_parallel_group(co_spawn(ex, std::move(t), deferred), + co_spawn(ex, std::move(u), deferred)) + .async_wait(wait_for_one_error(), deferred); + + if (ex0 && ex1) + throw multiple_exceptions(ex0); + if (ex0) + std::rethrow_exception(ex0); + if (ex1) + std::rethrow_exception(ex1); + co_return; +} + +/// Wait for both operations to succeed. +/** + * If one operations fails, the other is cancelled as the AND-condition can no + * longer be satisfied. + */ +template +traced_awaitable operator&&(traced_awaitable t, + traced_awaitable u) { + auto ex = co_await this_coro::executor; + + auto [order, ex0, ex1, r1] = + co_await make_parallel_group( + co_spawn(ex, std::move(t), deferred), + co_spawn(ex, detail::awaitable_wrap(std::move(u)), deferred)) + .async_wait(wait_for_one_error(), deferred); + + if (ex0 && ex1) + throw multiple_exceptions(ex0); + if (ex0) + std::rethrow_exception(ex0); + if (ex1) + std::rethrow_exception(ex1); + co_return std::move(detail::awaitable_unwrap(r1)); +} + +/// Wait for both operations to succeed. +/** + * If one operations fails, the other is cancelled as the AND-condition can no + * longer be satisfied. + */ +template +traced_awaitable operator&&(traced_awaitable t, + traced_awaitable u) { + auto ex = co_await this_coro::executor; + + auto [order, ex0, r0, ex1] = + co_await make_parallel_group( + co_spawn(ex, detail::awaitable_wrap(std::move(t)), deferred), + co_spawn(ex, std::move(u), deferred)) + .async_wait(wait_for_one_error(), deferred); + + if (ex0 && ex1) + throw multiple_exceptions(ex0); + if (ex0) + std::rethrow_exception(ex0); + if (ex1) + std::rethrow_exception(ex1); + co_return std::move(detail::awaitable_unwrap(r0)); +} + +/// Wait for both operations to succeed. +/** + * If one operations fails, the other is cancelled as the AND-condition can no + * longer be satisfied. + */ +template +traced_awaitable, Executor> +operator&&(traced_awaitable t, traced_awaitable u) { + auto ex = co_await this_coro::executor; + + auto [order, ex0, r0, ex1, r1] = + co_await make_parallel_group( + co_spawn(ex, detail::awaitable_wrap(std::move(t)), deferred), + co_spawn(ex, detail::awaitable_wrap(std::move(u)), deferred)) + .async_wait(wait_for_one_error(), deferred); + + if (ex0 && ex1) + throw multiple_exceptions(ex0); + if (ex0) + std::rethrow_exception(ex0); + if (ex1) + std::rethrow_exception(ex1); + co_return std::make_tuple(std::move(detail::awaitable_unwrap(r0)), + std::move(detail::awaitable_unwrap(r1))); +} + +/// Wait for both operations to succeed. +/** + * If one operations fails, the other is cancelled as the AND-condition can no + * longer be satisfied. + */ +template +traced_awaitable, Executor> +operator&&(traced_awaitable, Executor> t, + traced_awaitable u) { + auto ex = co_await this_coro::executor; + + auto [order, ex0, r0, ex1, r1] = + co_await make_parallel_group( + co_spawn(ex, detail::awaitable_wrap(std::move(t)), deferred), + co_spawn(ex, std::move(u), deferred)) + .async_wait(wait_for_one_error(), deferred); + + if (ex0 && ex1) + throw multiple_exceptions(ex0); + if (ex0) + std::rethrow_exception(ex0); + if (ex1) + std::rethrow_exception(ex1); + co_return std::move(detail::awaitable_unwrap>(r0)); +} + +/// Wait for both operations to succeed. +/** + * If one operations fails, the other is cancelled as the AND-condition can no + * longer be satisfied. + */ +template +traced_awaitable, Executor> +operator&&(traced_awaitable, Executor> t, + traced_awaitable u) { + auto ex = co_await this_coro::executor; + + auto [order, ex0, r0, ex1, r1] = + co_await make_parallel_group( + co_spawn(ex, detail::awaitable_wrap(std::move(t)), deferred), + co_spawn(ex, detail::awaitable_wrap(std::move(u)), deferred)) + .async_wait(wait_for_one_error(), deferred); + + if (ex0 && ex1) + throw multiple_exceptions(ex0); + if (ex0) + std::rethrow_exception(ex0); + if (ex1) + std::rethrow_exception(ex1); + co_return std::tuple_cat( + std::move(detail::awaitable_unwrap>(r0)), + std::make_tuple(std::move(detail::awaitable_unwrap(r1)))); +} + +/// Wait for one operation to succeed. +/** + * If one operations succeeds, the other is cancelled as the OR-condition is + * already satisfied. + */ +template +traced_awaitable, Executor> +operator||(traced_awaitable t, + traced_awaitable u) { + auto ex = co_await this_coro::executor; + + auto [order, ex0, ex1] = + co_await make_parallel_group(co_spawn(ex, std::move(t), deferred), + co_spawn(ex, std::move(u), deferred)) + .async_wait(wait_for_one_success(), deferred); + + if (order[0] == 0) { + if (!ex0) + co_return std::variant{ + std::in_place_index<0>}; + if (!ex1) + co_return std::variant{ + std::in_place_index<1>}; + throw multiple_exceptions(ex0); + } else { + if (!ex1) + co_return std::variant{ + std::in_place_index<1>}; + if (!ex0) + co_return std::variant{ + std::in_place_index<0>}; + throw multiple_exceptions(ex1); + } +} + +/// Wait for one operation to succeed. +/** + * If one operations succeeds, the other is cancelled as the OR-condition is + * already satisfied. + */ +template +traced_awaitable, Executor> +operator||(traced_awaitable t, + traced_awaitable u) { + auto ex = co_await this_coro::executor; + + auto [order, ex0, ex1, r1] = + co_await make_parallel_group( + co_spawn(ex, std::move(t), deferred), + co_spawn(ex, detail::awaitable_wrap(std::move(u)), deferred)) + .async_wait(wait_for_one_success(), deferred); + + if (order[0] == 0) { + if (!ex0) + co_return std::variant{std::in_place_index<0>}; + if (!ex1) + co_return std::variant{ + std::in_place_index<1>, + std::move(detail::awaitable_unwrap(r1))}; + throw multiple_exceptions(ex0); + } else { + if (!ex1) + co_return std::variant{ + std::in_place_index<1>, + std::move(detail::awaitable_unwrap(r1))}; + if (!ex0) + co_return std::variant{std::in_place_index<0>}; + throw multiple_exceptions(ex1); + } +} + +/// Wait for one operation to succeed. +/** + * If one operations succeeds, the other is cancelled as the OR-condition is + * already satisfied. + */ +template +traced_awaitable, Executor> +operator||(traced_awaitable t, + traced_awaitable u) { + auto ex = co_await this_coro::executor; + + auto [order, ex0, r0, ex1] = + co_await make_parallel_group( + co_spawn(ex, detail::awaitable_wrap(std::move(t)), deferred), + co_spawn(ex, std::move(u), deferred)) + .async_wait(wait_for_one_success(), deferred); + + if (order[0] == 0) { + if (!ex0) + co_return std::variant{ + std::in_place_index<0>, + std::move(detail::awaitable_unwrap(r0))}; + if (!ex1) + co_return std::variant{std::in_place_index<1>}; + throw multiple_exceptions(ex0); + } else { + if (!ex1) + co_return std::variant{std::in_place_index<1>}; + if (!ex0) + co_return std::variant{ + std::in_place_index<0>, + std::move(detail::awaitable_unwrap(r0))}; + throw multiple_exceptions(ex1); + } +} + +/// Wait for one operation to succeed. +/** + * If one operations succeeds, the other is cancelled as the OR-condition is + * already satisfied. + */ +template +traced_awaitable, Executor> +operator||(traced_awaitable t, traced_awaitable u) { + auto ex = co_await this_coro::executor; + + auto [order, ex0, r0, ex1, r1] = + co_await make_parallel_group( + co_spawn(ex, detail::awaitable_wrap(std::move(t)), deferred), + co_spawn(ex, detail::awaitable_wrap(std::move(u)), deferred)) + .async_wait(wait_for_one_success(), deferred); + + if (order[0] == 0) { + if (!ex0) + co_return std::variant{ + std::in_place_index<0>, + std::move(detail::awaitable_unwrap(r0))}; + if (!ex1) + co_return std::variant{ + std::in_place_index<1>, + std::move(detail::awaitable_unwrap(r1))}; + throw multiple_exceptions(ex0); + } else { + if (!ex1) + co_return std::variant{ + std::in_place_index<1>, + std::move(detail::awaitable_unwrap(r1))}; + if (!ex0) + co_return std::variant{ + std::in_place_index<0>, + std::move(detail::awaitable_unwrap(r0))}; + throw multiple_exceptions(ex1); + } +} + +namespace detail { + +template struct widen_variant { + template + static std::variant call(SourceVariant& source) { + if (source.index() == I) + return std::variant{std::in_place_index, + std::move(std::get(source))}; + else if constexpr (I + 1 < std::variant_size_v) + return call(source); + else + throw std::logic_error("empty variant"); + } +}; + +} // namespace detail + +/// Wait for one operation to succeed. +/** + * If one operations succeeds, the other is cancelled as the OR-condition is + * already satisfied. + */ +template +traced_awaitable, Executor> +operator||(traced_awaitable, Executor> t, + traced_awaitable u) { + auto ex = co_await this_coro::executor; + + auto [order, ex0, r0, ex1] = + co_await make_parallel_group( + co_spawn(ex, detail::awaitable_wrap(std::move(t)), deferred), + co_spawn(ex, std::move(u), deferred)) + .async_wait(wait_for_one_success(), deferred); + + using widen = detail::widen_variant; + if (order[0] == 0) { + if (!ex0) + co_return widen::template call<0>( + detail::awaitable_unwrap>(r0)); + if (!ex1) + co_return std::variant{ + std::in_place_index}; + throw multiple_exceptions(ex0); + } else { + if (!ex1) + co_return std::variant{ + std::in_place_index}; + if (!ex0) + co_return widen::template call<0>( + detail::awaitable_unwrap>(r0)); + throw multiple_exceptions(ex1); + } +} + +/// Wait for one operation to succeed. +/** + * If one operations succeeds, the other is cancelled as the OR-condition is + * already satisfied. + */ +template +traced_awaitable, Executor> +operator||(traced_awaitable, Executor> t, + traced_awaitable u) { + auto ex = co_await this_coro::executor; + + auto [order, ex0, r0, ex1, r1] = + co_await make_parallel_group( + co_spawn(ex, detail::awaitable_wrap(std::move(t)), deferred), + co_spawn(ex, detail::awaitable_wrap(std::move(u)), deferred)) + .async_wait(wait_for_one_success(), deferred); + + using widen = detail::widen_variant; + if (order[0] == 0) { + if (!ex0) + co_return widen::template call<0>( + detail::awaitable_unwrap>(r0)); + if (!ex1) + co_return std::variant{ + std::in_place_index, + std::move(detail::awaitable_unwrap(r1))}; + throw multiple_exceptions(ex0); + } else { + if (!ex1) + co_return std::variant{ + std::in_place_index, + std::move(detail::awaitable_unwrap(r1))}; + if (!ex0) + co_return widen::template call<0>( + detail::awaitable_unwrap>(r0)); + throw multiple_exceptions(ex1); + } +} + +} // namespace awaitable_operators +} // namespace experimental +} // namespace asio +} // namespace boost From 906d4a29cf9fa8f42edb81c86225095c9fb8e03f Mon Sep 17 00:00:00 2001 From: Sungsik Date: Tue, 16 Sep 2025 10:12:51 +0200 Subject: [PATCH 07/35] Added context propagation --- src/proxy/cache/awaitable_operators.h | 104 ++++++++++++++++++++------ 1 file changed, 80 insertions(+), 24 deletions(-) diff --git a/src/proxy/cache/awaitable_operators.h b/src/proxy/cache/awaitable_operators.h index 3884d9f6b..fbc7e91f6 100644 --- a/src/proxy/cache/awaitable_operators.h +++ b/src/proxy/cache/awaitable_operators.h @@ -47,10 +47,12 @@ traced_awaitable operator&&(traced_awaitable t, traced_awaitable u) { auto ex = co_await this_coro::executor; + auto context = co_await this_coro::context; auto [order, ex0, ex1] = - co_await make_parallel_group(co_spawn(ex, std::move(t), deferred), - co_spawn(ex, std::move(u), deferred)) + co_await make_parallel_group( + co_spawn(ex, std::move(t.continue_trace(context)), deferred), + co_spawn(ex, std::move(u.continue_trace(context)), deferred)) .async_wait(wait_for_one_error(), deferred); if (ex0 && ex1) @@ -71,11 +73,15 @@ template traced_awaitable operator&&(traced_awaitable t, traced_awaitable u) { auto ex = co_await this_coro::executor; + auto context = co_await this_coro::context; auto [order, ex0, ex1, r1] = co_await make_parallel_group( - co_spawn(ex, std::move(t), deferred), - co_spawn(ex, detail::awaitable_wrap(std::move(u)), deferred)) + co_spawn(ex, std::move(t.continue_trace(context)), deferred), + co_spawn( + ex, + detail::awaitable_wrap(std::move(u.continue_trace(context))), + deferred)) .async_wait(wait_for_one_error(), deferred); if (ex0 && ex1) @@ -96,11 +102,15 @@ template traced_awaitable operator&&(traced_awaitable t, traced_awaitable u) { auto ex = co_await this_coro::executor; + auto context = co_await this_coro::context; auto [order, ex0, r0, ex1] = co_await make_parallel_group( - co_spawn(ex, detail::awaitable_wrap(std::move(t)), deferred), - co_spawn(ex, std::move(u), deferred)) + co_spawn( + ex, + detail::awaitable_wrap(std::move(t.continue_trace(context))), + deferred), + co_spawn(ex, std::move(u.continue_trace(context)), deferred)) .async_wait(wait_for_one_error(), deferred); if (ex0 && ex1) @@ -121,11 +131,18 @@ template traced_awaitable, Executor> operator&&(traced_awaitable t, traced_awaitable u) { auto ex = co_await this_coro::executor; + auto context = co_await this_coro::context; auto [order, ex0, r0, ex1, r1] = co_await make_parallel_group( - co_spawn(ex, detail::awaitable_wrap(std::move(t)), deferred), - co_spawn(ex, detail::awaitable_wrap(std::move(u)), deferred)) + co_spawn( + ex, + detail::awaitable_wrap(std::move(t.continue_trace(context))), + deferred), + co_spawn( + ex, + detail::awaitable_wrap(std::move(u.continue_trace(context))), + deferred)) .async_wait(wait_for_one_error(), deferred); if (ex0 && ex1) @@ -148,11 +165,15 @@ traced_awaitable, Executor> operator&&(traced_awaitable, Executor> t, traced_awaitable u) { auto ex = co_await this_coro::executor; + auto context = co_await this_coro::context; auto [order, ex0, r0, ex1, r1] = co_await make_parallel_group( - co_spawn(ex, detail::awaitable_wrap(std::move(t)), deferred), - co_spawn(ex, std::move(u), deferred)) + co_spawn( + ex, + detail::awaitable_wrap(std::move(t.continue_trace(context))), + deferred), + co_spawn(ex, std::move(u.continue_trace(context)), deferred)) .async_wait(wait_for_one_error(), deferred); if (ex0 && ex1) @@ -174,11 +195,18 @@ traced_awaitable, Executor> operator&&(traced_awaitable, Executor> t, traced_awaitable u) { auto ex = co_await this_coro::executor; + auto context = co_await this_coro::context; auto [order, ex0, r0, ex1, r1] = co_await make_parallel_group( - co_spawn(ex, detail::awaitable_wrap(std::move(t)), deferred), - co_spawn(ex, detail::awaitable_wrap(std::move(u)), deferred)) + co_spawn( + ex, + detail::awaitable_wrap(std::move(t.continue_trace(context))), + deferred), + co_spawn( + ex, + detail::awaitable_wrap(std::move(u.continue_trace(context))), + deferred)) .async_wait(wait_for_one_error(), deferred); if (ex0 && ex1) @@ -202,10 +230,12 @@ traced_awaitable, Executor> operator||(traced_awaitable t, traced_awaitable u) { auto ex = co_await this_coro::executor; + auto context = co_await this_coro::context; auto [order, ex0, ex1] = - co_await make_parallel_group(co_spawn(ex, std::move(t), deferred), - co_spawn(ex, std::move(u), deferred)) + co_await make_parallel_group( + co_spawn(ex, std::move(t.continue_trace(context)), deferred), + co_spawn(ex, std::move(u.continue_trace(context)), deferred)) .async_wait(wait_for_one_success(), deferred); if (order[0] == 0) { @@ -237,11 +267,15 @@ traced_awaitable, Executor> operator||(traced_awaitable t, traced_awaitable u) { auto ex = co_await this_coro::executor; + auto context = co_await this_coro::context; auto [order, ex0, ex1, r1] = co_await make_parallel_group( - co_spawn(ex, std::move(t), deferred), - co_spawn(ex, detail::awaitable_wrap(std::move(u)), deferred)) + co_spawn(ex, std::move(t.continue_trace(context)), deferred), + co_spawn( + ex, + detail::awaitable_wrap(std::move(u.continue_trace(context))), + deferred)) .async_wait(wait_for_one_success(), deferred); if (order[0] == 0) { @@ -273,11 +307,15 @@ traced_awaitable, Executor> operator||(traced_awaitable t, traced_awaitable u) { auto ex = co_await this_coro::executor; + auto context = co_await this_coro::context; auto [order, ex0, r0, ex1] = co_await make_parallel_group( - co_spawn(ex, detail::awaitable_wrap(std::move(t)), deferred), - co_spawn(ex, std::move(u), deferred)) + co_spawn( + ex, + detail::awaitable_wrap(std::move(t.continue_trace(context))), + deferred), + co_spawn(ex, std::move(u.continue_trace(context)), deferred)) .async_wait(wait_for_one_success(), deferred); if (order[0] == 0) { @@ -308,11 +346,18 @@ template traced_awaitable, Executor> operator||(traced_awaitable t, traced_awaitable u) { auto ex = co_await this_coro::executor; + auto context = co_await this_coro::context; auto [order, ex0, r0, ex1, r1] = co_await make_parallel_group( - co_spawn(ex, detail::awaitable_wrap(std::move(t)), deferred), - co_spawn(ex, detail::awaitable_wrap(std::move(u)), deferred)) + co_spawn( + ex, + detail::awaitable_wrap(std::move(t.continue_trace(context))), + deferred), + co_spawn( + ex, + detail::awaitable_wrap(std::move(u.continue_trace(context))), + deferred)) .async_wait(wait_for_one_success(), deferred); if (order[0] == 0) { @@ -365,11 +410,15 @@ traced_awaitable, Executor> operator||(traced_awaitable, Executor> t, traced_awaitable u) { auto ex = co_await this_coro::executor; + auto context = co_await this_coro::context; auto [order, ex0, r0, ex1] = co_await make_parallel_group( - co_spawn(ex, detail::awaitable_wrap(std::move(t)), deferred), - co_spawn(ex, std::move(u), deferred)) + co_spawn( + ex, + detail::awaitable_wrap(std::move(t.continue_trace(context))), + deferred), + co_spawn(ex, std::move(u.continue_trace(context)), deferred)) .async_wait(wait_for_one_success(), deferred); using widen = detail::widen_variant; @@ -402,11 +451,18 @@ traced_awaitable, Executor> operator||(traced_awaitable, Executor> t, traced_awaitable u) { auto ex = co_await this_coro::executor; + auto context = co_await this_coro::context; auto [order, ex0, r0, ex1, r1] = co_await make_parallel_group( - co_spawn(ex, detail::awaitable_wrap(std::move(t)), deferred), - co_spawn(ex, detail::awaitable_wrap(std::move(u)), deferred)) + co_spawn( + ex, + detail::awaitable_wrap(std::move(t.continue_trace(context))), + deferred), + co_spawn( + ex, + detail::awaitable_wrap(std::move(u.continue_trace(context))), + deferred)) .async_wait(wait_for_one_success(), deferred); using widen = detail::widen_variant; From 4bcc659112743e765f62210922d530453fa76faa Mon Sep 17 00:00:00 2001 From: Sungsik Date: Thu, 18 Sep 2025 10:49:16 +0200 Subject: [PATCH 08/35] Test double_buffered_writer_body --- test/unit/test_disk_cache_body.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/test_disk_cache_body.cpp b/test/unit/test_disk_cache_body.cpp index 520ccc11d..5869491ed 100644 --- a/test/unit/test_disk_cache_body.cpp +++ b/test/unit/test_disk_cache_body.cpp @@ -106,7 +106,7 @@ BOOST_AUTO_TEST_CASE(supports_write) { auto objh = std::make_shared(std::move(addr)); BOOST_TEST(objh->data_size() == data.size()); - double_buffered_writer_body body(data_view, std::move(objh), 16); + double_buffered_writer_body body(data_view, std::move(objh)); // Set up TCP sockets boost::asio::ip::tcp::acceptor acceptor(m_ioc, From 29e97ebafe90db14ff4b66d6d7f7df389f17d6fe Mon Sep 17 00:00:00 2001 From: Sungsik Date: Thu, 18 Sep 2025 10:50:06 +0200 Subject: [PATCH 09/35] Optimized buffer size of cache's writer body --- src/proxy/cache/disk/body.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/proxy/cache/disk/body.h b/src/proxy/cache/disk/body.h index 892f8cb81..27fb3cdf2 100644 --- a/src/proxy/cache/disk/body.h +++ b/src/proxy/cache/disk/body.h @@ -53,8 +53,7 @@ class writer_body { using support_double_buffer = std::false_type; writer_body(storage::data_view& storage, - std::shared_ptr objh, - std::size_t buffer_size = 32 * MEBI_BYTE) + std::shared_ptr objh, std::size_t buffer_size) : m_storage(storage), m_objh{std::move(objh)}, m_buffer(buffer_size) {} @@ -116,7 +115,7 @@ class double_buffered_writer_body : private writer_body { double_buffered_writer_body(storage::data_view& storage, std::shared_ptr objh, - std::size_t buffer_size = 32 * MEBI_BYTE) + std::size_t buffer_size = 16_MiB) : writer_body(storage, objh, buffer_size), m_buffer2(buffer_size), m_active(&m_buffer), From b9358d91e7d6e0f17ce0f81567ba0da64c6115a1 Mon Sep 17 00:00:00 2001 From: Sungsik Date: Thu, 18 Sep 2025 10:50:28 +0200 Subject: [PATCH 10/35] Test relay --- test/unit/test_beast.cpp | 167 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) diff --git a/test/unit/test_beast.cpp b/test/unit/test_beast.cpp index 9c45406e2..0103600cc 100644 --- a/test/unit/test_beast.cpp +++ b/test/unit/test_beast.cpp @@ -333,3 +333,170 @@ BOOST_AUTO_TEST_CASE(supports_vector_of_const_buffers) { BOOST_AUTO_TEST_SUITE_END() } // namespace uh::cluster + +using namespace boost::beast::http; +#include "double_buffer_body.h" +#include + +/** Relay an HTTP message. + + This function efficiently relays an HTTP message from a downstream + client to an upstream server, or from an upstream server to a + downstream client. After the message header is read from the input, + a user provided transformation function is invoked which may change + the contents of the header before forwarding to the output. This may + be used to adjust fields such as Server, or proxy fields. + + @param output The stream to write to. + + @param input The stream to read from. + + @param buffer The buffer to use for the input. + + @param transform The header transformation to apply. The function will + be called with this signature: + @code + template + void transform(message< + isRequest, Body, Fields>&, // The message to transform + error_code&); // Set to the error, if any + @endcode + + @param ec Set to the error if any occurred. + + @tparam isRequest `true` to relay a request. + + @tparam Fields The type of fields to use for the message. +*/ +template +void relay(SyncWriteStream& output, SyncReadStream& input, + DynamicBuffer& buffer, boost::system::error_code& ec, + Transform&& transform) { + static_assert(is_sync_write_stream::value, + "SyncWriteStream requirements not met"); + + static_assert(is_sync_read_stream::value, + "SyncReadStream requirements not met"); + + // A small buffer for relaying the body piece by piece + constexpr std::size_t buf_size = 2048; + char buf[2][buf_size]; + char* rbuf = buf[0]; + (void)rbuf; + char* wbuf = buf[1]; + + // Create a parser with a buffer body to read from the input. + parser p; + + // Create a serializer from the message contained in the parser. + serializer sr{p.get()}; + + // Read just the header from the input + read_header(input, buffer, p, ec); + if (ec) + return; + + // Apply the caller's header transformation + transform(p.get(), ec); + if (ec) + return; + + // Send the transformed message to the output + write_header(output, sr, ec); + if (ec) + return; + + // Loop over the input and transfer it to the output + do { + if (!p.is_done()) { + // Set up the body for writing into our small buffer + p.get().body().rdata = wbuf; + p.get().body().rsize = buf_size; + + // Read as much as we can + read(input, buffer, p, ec); + + // This error is returned when double_buffer_body uses up the buffer + if (ec == http::error::need_buffer) + ec = {}; + if (ec) + return; + + // Set up the body for reading. + // This is how much was parsed: + p.get().body().wsize = buf_size - p.get().body().rsize; + p.get().body().wdata = wbuf; + p.get().body().more = !p.is_done(); + } else { + p.get().body().wdata = nullptr; + p.get().body().wsize = 0; + } + + // Write everything in the buffer (which might be empty) + write(output, sr, ec); + + // This error is returned when double_buffer_body uses up the buffer + if (ec == http::error::need_buffer) + ec = {}; + if (ec) + return; + } while (!p.is_done() && !sr.is_done()); +} + +#include + +BOOST_AUTO_TEST_CASE(supports_buffer_body_with_socket) { + using namespace boost::asio; + using namespace boost::beast::http; + + io_context ioc; + ip::tcp::acceptor acceptor(ioc, ip::tcp::endpoint(ip::tcp::v4(), 0)); + ip::tcp::endpoint endpoint = acceptor.local_endpoint(); + + ip::tcp::socket server_socket(ioc); + ip::tcp::socket client_socket(ioc); + + std::thread server_thread([&] { acceptor.accept(server_socket); }); + + client_socket.connect(endpoint); + server_thread.join(); + + std::vector body(8 * 1024); + std::mt19937 rng{std::random_device{}()}; + std::uniform_int_distribution dist(0, 255); + for (auto& c : body) + c = static_cast(dist(rng)); + + std::string request = "POST /upload HTTP/1.1\r\n" + "Host: example.com\r\n" + "User-Agent: test\r\n" + "Content-Length: " + + std::to_string(body.size()) + "\r\n\r\n"; + + write(client_socket, buffer(request)); + write(client_socket, buffer(body)); + + flat_buffer b; + boost::system::error_code ec; + auto transform = [](auto&, boost::system::error_code&) {}; + + relay(server_socket, server_socket, b, ec, transform); + + BOOST_TEST(!ec); + + std::vector recv_buf(16 * 1024); + size_t n = client_socket.read_some(buffer(recv_buf), ec); + std::string output_str(recv_buf.data(), n); + + BOOST_CHECK_NE(output_str.find("POST /upload HTTP/1.1"), std::string::npos); + BOOST_CHECK_NE(output_str.find("Host: example.com"), std::string::npos); + BOOST_CHECK_NE(output_str.find("User-Agent: test"), std::string::npos); + + // body 검증 + auto body_pos = output_str.find("\r\n\r\n"); + BOOST_REQUIRE(body_pos != std::string::npos); + body_pos += 4; + std::string_view received_body(&recv_buf[body_pos], n - body_pos); + BOOST_TEST(received_body == std::string_view(body.data(), body.size())); +} From a15339dc28cbd856958e0cb98ecab90c5b2319a1 Mon Sep 17 00:00:00 2001 From: Sungsik Date: Thu, 18 Sep 2025 11:12:22 +0200 Subject: [PATCH 11/35] Test relay using coroutine --- test/unit/test_beast.cpp | 66 ++++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/test/unit/test_beast.cpp b/test/unit/test_beast.cpp index 0103600cc..0140a0dfd 100644 --- a/test/unit/test_beast.cpp +++ b/test/unit/test_beast.cpp @@ -337,7 +337,9 @@ BOOST_AUTO_TEST_SUITE_END() using namespace boost::beast::http; #include "double_buffer_body.h" #include +#include +namespace uh::cluster { /** Relay an HTTP message. This function efficiently relays an HTTP message from a downstream @@ -370,9 +372,8 @@ using namespace boost::beast::http; */ template -void relay(SyncWriteStream& output, SyncReadStream& input, - DynamicBuffer& buffer, boost::system::error_code& ec, - Transform&& transform) { +coro relay(SyncWriteStream& output, SyncReadStream& input, + DynamicBuffer& buffer, Transform&& transform) { static_assert(is_sync_write_stream::value, "SyncWriteStream requirements not met"); @@ -393,19 +394,13 @@ void relay(SyncWriteStream& output, SyncReadStream& input, serializer sr{p.get()}; // Read just the header from the input - read_header(input, buffer, p, ec); - if (ec) - return; + co_await async_read_header(input, buffer, p); // Apply the caller's header transformation - transform(p.get(), ec); - if (ec) - return; + transform(p.get()); // Send the transformed message to the output - write_header(output, sr, ec); - if (ec) - return; + co_await async_write_header(output, sr); // Loop over the input and transfer it to the output do { @@ -415,13 +410,15 @@ void relay(SyncWriteStream& output, SyncReadStream& input, p.get().body().rsize = buf_size; // Read as much as we can - read(input, buffer, p, ec); + try { + co_await async_read(input, buffer, p); - // This error is returned when double_buffer_body uses up the buffer - if (ec == http::error::need_buffer) - ec = {}; - if (ec) - return; + } catch (const boost::system::system_error& e) { + if (e.code() != http::error::need_buffer) { + std::cerr << "Error during read: " << e.what() << std::endl; + throw; + } + } // Set up the body for reading. // This is how much was parsed: @@ -434,18 +431,22 @@ void relay(SyncWriteStream& output, SyncReadStream& input, } // Write everything in the buffer (which might be empty) - write(output, sr, ec); - - // This error is returned when double_buffer_body uses up the buffer - if (ec == http::error::need_buffer) - ec = {}; - if (ec) - return; + try { + co_await async_write(output, sr); + + } catch (const boost::system::system_error& e) { + if (e.code() != http::error::need_buffer) { + std::cerr << "Error during read: " << e.what() << std::endl; + throw; + } + } } while (!p.is_done() && !sr.is_done()); } +} // namespace uh::cluster #include +namespace uh::cluster { BOOST_AUTO_TEST_CASE(supports_buffer_body_with_socket) { using namespace boost::asio; using namespace boost::beast::http; @@ -478,13 +479,18 @@ BOOST_AUTO_TEST_CASE(supports_buffer_body_with_socket) { write(client_socket, buffer(body)); flat_buffer b; - boost::system::error_code ec; - auto transform = [](auto&, boost::system::error_code&) {}; + auto transform = [](auto&) {}; - relay(server_socket, server_socket, b, ec, transform); + auto work_guard = boost::asio::make_work_guard(ioc.get_executor()); + auto thread = std::thread([&ioc] { ioc.run(); }); + co_spawn(ioc, relay(server_socket, server_socket, b, transform), + boost::asio::use_future) + .get(); - BOOST_TEST(!ec); + work_guard.reset(); + thread.join(); + boost::system::error_code ec; std::vector recv_buf(16 * 1024); size_t n = client_socket.read_some(buffer(recv_buf), ec); std::string output_str(recv_buf.data(), n); @@ -493,10 +499,10 @@ BOOST_AUTO_TEST_CASE(supports_buffer_body_with_socket) { BOOST_CHECK_NE(output_str.find("Host: example.com"), std::string::npos); BOOST_CHECK_NE(output_str.find("User-Agent: test"), std::string::npos); - // body 검증 auto body_pos = output_str.find("\r\n\r\n"); BOOST_REQUIRE(body_pos != std::string::npos); body_pos += 4; std::string_view received_body(&recv_buf[body_pos], n - body_pos); BOOST_TEST(received_body == std::string_view(body.data(), body.size())); } +} // namespace uh::cluster From 2fab9bf71bbb29cc4cde1abaa17ca3b779c8f56d Mon Sep 17 00:00:00 2001 From: Sungsik Date: Thu, 18 Sep 2025 11:23:12 +0200 Subject: [PATCH 12/35] Wrap ignoring need_buffer --- test/unit/test_beast.cpp | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/test/unit/test_beast.cpp b/test/unit/test_beast.cpp index 0140a0dfd..25ba1ea08 100644 --- a/test/unit/test_beast.cpp +++ b/test/unit/test_beast.cpp @@ -340,6 +340,15 @@ using namespace boost::beast::http; #include namespace uh::cluster { + +template coro ignore_need_buffer(Awaitable&& op) { + boost::system::error_code ec; + co_await op(boost::asio::redirect_error(boost::asio::use_awaitable, ec)); + if (ec && ec != boost::beast::http::error::need_buffer) { + throw boost::system::system_error(ec); + } +} + /** Relay an HTTP message. This function efficiently relays an HTTP message from a downstream @@ -410,15 +419,9 @@ coro relay(SyncWriteStream& output, SyncReadStream& input, p.get().body().rsize = buf_size; // Read as much as we can - try { - co_await async_read(input, buffer, p); - - } catch (const boost::system::system_error& e) { - if (e.code() != http::error::need_buffer) { - std::cerr << "Error during read: " << e.what() << std::endl; - throw; - } - } + co_await ignore_need_buffer([&](auto token) { + return async_read(input, buffer, p, token); + }); // Set up the body for reading. // This is how much was parsed: @@ -431,15 +434,9 @@ coro relay(SyncWriteStream& output, SyncReadStream& input, } // Write everything in the buffer (which might be empty) - try { - co_await async_write(output, sr); - - } catch (const boost::system::system_error& e) { - if (e.code() != http::error::need_buffer) { - std::cerr << "Error during read: " << e.what() << std::endl; - throw; - } - } + co_await ignore_need_buffer( + [&](auto token) { return async_write(output, sr, token); }); + } while (!p.is_done() && !sr.is_done()); } } // namespace uh::cluster From fa0800f09714e322028fb135710e5edc90867afb Mon Sep 17 00:00:00 2001 From: Sungsik Date: Thu, 18 Sep 2025 11:28:56 +0200 Subject: [PATCH 13/35] Use async_stream instead sync --- test/unit/test_beast.cpp | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/test/unit/test_beast.cpp b/test/unit/test_beast.cpp index 25ba1ea08..7687f8ee9 100644 --- a/test/unit/test_beast.cpp +++ b/test/unit/test_beast.cpp @@ -379,15 +379,14 @@ template coro ignore_need_buffer(Awaitable&& op) { @tparam Fields The type of fields to use for the message. */ -template -coro relay(SyncWriteStream& output, SyncReadStream& input, +coro relay(AsyncWriteStream& output, AsyncReadStream& input, DynamicBuffer& buffer, Transform&& transform) { - static_assert(is_sync_write_stream::value, - "SyncWriteStream requirements not met"); - - static_assert(is_sync_read_stream::value, - "SyncReadStream requirements not met"); + static_assert(boost::beast::is_async_write_stream::value, + "AsyncWriteStream requirements not met"); + static_assert(boost::beast::is_async_read_stream::value, + "AsyncReadStream requirements not met"); // A small buffer for relaying the body piece by piece constexpr std::size_t buf_size = 2048; From e7b47eb3bafd43c4bee022b58613d684b1895542 Mon Sep 17 00:00:00 2001 From: Sungsik Date: Thu, 18 Sep 2025 13:13:56 +0200 Subject: [PATCH 14/35] Refactored for pipelining --- test/unit/test_beast.cpp | 55 +++++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/test/unit/test_beast.cpp b/test/unit/test_beast.cpp index 7687f8ee9..254c33b96 100644 --- a/test/unit/test_beast.cpp +++ b/test/unit/test_beast.cpp @@ -338,6 +338,9 @@ using namespace boost::beast::http; #include "double_buffer_body.h" #include #include +#include + +using namespace boost::asio::experimental::awaitable_operators; namespace uh::cluster { @@ -390,10 +393,10 @@ coro relay(AsyncWriteStream& output, AsyncReadStream& input, // A small buffer for relaying the body piece by piece constexpr std::size_t buf_size = 2048; - char buf[2][buf_size]; - char* rbuf = buf[0]; + char _buf[2][buf_size]; + char* rbuf = _buf[0]; (void)rbuf; - char* wbuf = buf[1]; + char* wbuf = _buf[1]; // Create a parser with a buffer body to read from the input. parser p; @@ -411,31 +414,37 @@ coro relay(AsyncWriteStream& output, AsyncReadStream& input, co_await async_write_header(output, sr); // Loop over the input and transfer it to the output - do { - if (!p.is_done()) { - // Set up the body for writing into our small buffer - p.get().body().rdata = wbuf; - p.get().body().rsize = buf_size; - - // Read as much as we can - co_await ignore_need_buffer([&](auto token) { - return async_read(input, buffer, p, token); - }); - - // Set up the body for reading. - // This is how much was parsed: - p.get().body().wsize = buf_size - p.get().body().rsize; - p.get().body().wdata = wbuf; - p.get().body().more = !p.is_done(); - } else { - p.get().body().wdata = nullptr; - p.get().body().wsize = 0; + auto read = [&](char* buf) -> coro { + if (p.is_done()) { + co_return false; } - // Write everything in the buffer (which might be empty) + p.get().body().rdata = buf; + p.get().body().rsize = buf_size; + + // Read as much as we can + co_await ignore_need_buffer( + [&](auto token) { return async_read(input, buffer, p, token); }); + + co_return true; + }; + + auto write = [&](bool more, char* buf, std::size_t size) -> coro { + p.get().body().more = more; + p.get().body().wdata = more ? buf : nullptr; + p.get().body().wsize = more ? size : 0; co_await ignore_need_buffer( [&](auto token) { return async_write(output, sr, token); }); + }; + // for (auto more = co_await read(rbuf); !p.is_done() && !sr.is_done();) { + // std::swap(rbuf, wbuf); + // co_await (read(rbuf) && + // write(more, wbuf, buf_size - p.get().body().rsize)); + // } + do { + auto more = co_await read(wbuf); + co_await write(more, wbuf, buf_size - p.get().body().rsize); } while (!p.is_done() && !sr.is_done()); } } // namespace uh::cluster From 893e865af12bc755cd86599f4d3bc92603738e53 Mon Sep 17 00:00:00 2001 From: Sungsik Date: Thu, 18 Sep 2025 14:17:41 +0200 Subject: [PATCH 15/35] Pipelining done --- test/unit/double_buffer_body.h | 193 +++++++++++++++++++++++++++++++++ test/unit/test_beast.cpp | 47 +++----- 2 files changed, 208 insertions(+), 32 deletions(-) create mode 100644 test/unit/double_buffer_body.h diff --git a/test/unit/double_buffer_body.h b/test/unit/double_buffer_body.h new file mode 100644 index 000000000..201bce3cf --- /dev/null +++ b/test/unit/double_buffer_body.h @@ -0,0 +1,193 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace boost { +namespace beast { +namespace http { + +/** A Body using a caller provided buffer + + Messages using this body type may be serialized and parsed. + To use this class, the caller must initialize the members + of @ref buffer_body::value_type to appropriate values before + each call to read or write during a stream operation. +*/ +struct double_buffer_body { + /// The type of the body member when used in a message. + struct value_type { + /** A pointer to a contiguous area of memory of @ref rsize octets, else + `nullptr`. + + @par Only for Parsing + + If this is `nullptr`, the error @ref error::need_buffer + will be returned from @ref parser::put. Otherwise, the + parser will store body octets into the memory pointed to + by `rdata` having `rsize` octets of valid storage. After + octets are stored, the `rdata` and `rsize` members are + adjusted: `rdata` is incremented to point to the next + octet after the rdata written, while `rsize` is decremented + to reflect the remaining space at the memory location + pointed to by `rdata`. + */ + void* rdata = nullptr; + + /** The number of octets in the buffer pointed to by @ref rdata. + + @par Only for Parsing + + The value of this field will be decremented during parsing + to indicate the number of remaining free octets in the + buffer pointed to by `rdata`. When it reaches zero, the + parser will return @ref error::need_buffer, indicating to + the caller that the values of `rdata` and `rsize` should be + updated to point to a new memory buffer. + */ + std::size_t rsize = 0; + + /** A pointer to a contiguous area of memory of @ref wsize octets, else + `nullptr`. + + @par Only for Serializing + + If this is `nullptr` and `more` is `true`, the error + @ref error::need_buffer will be returned from @ref serializer::get + Otherwise, the serializer will use the memory pointed to + by `wdata` having `wsize` octets of valid storage as the + next buffer representing the body. + */ + void* wdata = nullptr; + + /** The number of octets in the buffer pointed to by @ref wdata. + + @par Only for Serializing + + If `wdata` is `nullptr` during serialization, this value + is ignored. Otherwise, it represents the number of valid + body octets pointed to by `wdata`. + */ + std::size_t wsize = 0; + + /** `true` if this is not the last buffer. + + @par When Serializing + + If this is `true` and `wdata` is `nullptr`, the error + @ref error::need_buffer will be returned from @ref serializer::get + + @par When Parsing + + This field is not used during parsing. + */ + bool more = true; + }; + + /** The algorithm for parsing the body + + Meets the requirements of BodyReader. + */ +#if BOOST_BEAST_DOXYGEN + using reader = __implementation_defined__; +#else + class reader { + value_type& body_; + + public: + template + explicit reader(header&, value_type& b) + : body_(b) {} + + void init(boost::optional const&, error_code& ec) { + ec = {}; + } + + template + std::size_t put(ConstBufferSequence const& buffers, error_code& ec) { + if (!body_.rdata) { + BOOST_BEAST_ASSIGN_EC(ec, error::need_buffer); + return 0; + } + auto const bytes_transferred = net::buffer_copy( + net::buffer(body_.rdata, body_.rsize), buffers); + body_.rdata = static_cast(body_.rdata) + bytes_transferred; + body_.rsize -= bytes_transferred; + if (bytes_transferred == buffer_bytes(buffers)) + ec = {}; + else { + BOOST_BEAST_ASSIGN_EC(ec, error::need_buffer); + } + return bytes_transferred; + } + + void finish(error_code& ec) { ec = {}; } + }; +#endif + + /** The algorithm for serializing the body + + Meets the requirements of BodyWriter. + */ +#if BOOST_BEAST_DOXYGEN + using writer = __implementation_defined__; +#else + class writer { + bool toggle_ = false; + value_type const& body_; + + public: + using const_buffers_type = net::const_buffer; + + template + explicit writer(header const&, value_type const& b) + : body_(b) {} + + void init(error_code& ec) { ec = {}; } + + boost::optional> + get(error_code& ec) { + if (toggle_) { + if (body_.more) { + toggle_ = false; + BOOST_BEAST_ASSIGN_EC(ec, error::need_buffer); + } else { + ec = {}; + } + return boost::none; + } + if (body_.wdata) { + ec = {}; + toggle_ = true; + return { + {const_buffers_type{body_.wdata, body_.wsize}, body_.more}}; + } + if (body_.more) { + BOOST_BEAST_ASSIGN_EC(ec, error::need_buffer); + } else + ec = {}; + return boost::none; + } + }; +#endif +}; + +#if !BOOST_BEAST_DOXYGEN +// operator<< is not supported for double_buffer_body +template +std::ostream& +operator<<(std::ostream& os, + message const& msg) = delete; +#endif + +} // namespace http +} // namespace beast +} // namespace boost diff --git a/test/unit/test_beast.cpp b/test/unit/test_beast.cpp index 254c33b96..c0325c38d 100644 --- a/test/unit/test_beast.cpp +++ b/test/unit/test_beast.cpp @@ -338,6 +338,7 @@ using namespace boost::beast::http; #include "double_buffer_body.h" #include #include +#include #include using namespace boost::asio::experimental::awaitable_operators; @@ -391,61 +392,43 @@ coro relay(AsyncWriteStream& output, AsyncReadStream& input, static_assert(boost::beast::is_async_read_stream::value, "AsyncReadStream requirements not met"); - // A small buffer for relaying the body piece by piece - constexpr std::size_t buf_size = 2048; + constexpr std::size_t buf_size = 2_KiB; char _buf[2][buf_size]; char* rbuf = _buf[0]; - (void)rbuf; char* wbuf = _buf[1]; - // Create a parser with a buffer body to read from the input. parser p; - - // Create a serializer from the message contained in the parser. serializer sr{p.get()}; - // Read just the header from the input co_await async_read_header(input, buffer, p); - - // Apply the caller's header transformation transform(p.get()); - - // Send the transformed message to the output co_await async_write_header(output, sr); - // Loop over the input and transfer it to the output - auto read = [&](char* buf) -> coro { - if (p.is_done()) { - co_return false; - } + auto read = [&](char* buf) -> coro { + if (p.is_done()) + co_return 0; p.get().body().rdata = buf; p.get().body().rsize = buf_size; - - // Read as much as we can co_await ignore_need_buffer( [&](auto token) { return async_read(input, buffer, p, token); }); - co_return true; + co_return buf_size - p.get().body().rsize; }; - auto write = [&](bool more, char* buf, std::size_t size) -> coro { - p.get().body().more = more; - p.get().body().wdata = more ? buf : nullptr; - p.get().body().wsize = more ? size : 0; + auto write = [&](char* buf, std::size_t size) -> coro { + p.get().body().more = size != 0; + p.get().body().wdata = buf; + p.get().body().wsize = size; co_await ignore_need_buffer( [&](auto token) { return async_write(output, sr, token); }); }; - // for (auto more = co_await read(rbuf); !p.is_done() && !sr.is_done();) { - // std::swap(rbuf, wbuf); - // co_await (read(rbuf) && - // write(more, wbuf, buf_size - p.get().body().rsize)); - // } - do { - auto more = co_await read(wbuf); - co_await write(more, wbuf, buf_size - p.get().body().rsize); - } while (!p.is_done() && !sr.is_done()); + for (auto bytes_read = co_await read(rbuf); + !p.is_done() || !sr.is_done();) { + std::swap(rbuf, wbuf); + bytes_read = co_await (read(rbuf) && write(wbuf, bytes_read)); + } } } // namespace uh::cluster From 30e4ed24506a7d7ecfbfd388adaf5b794f59d756 Mon Sep 17 00:00:00 2001 From: Sungsik Date: Thu, 18 Sep 2025 16:31:17 +0200 Subject: [PATCH 16/35] Refactor relay function: take `relay header` out --- test/unit/test_beast.cpp | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/test/unit/test_beast.cpp b/test/unit/test_beast.cpp index c0325c38d..aedb3c25e 100644 --- a/test/unit/test_beast.cpp +++ b/test/unit/test_beast.cpp @@ -384,9 +384,9 @@ template coro ignore_need_buffer(Awaitable&& op) { @tparam Fields The type of fields to use for the message. */ template + class DynamicBuffer, class Parser, class Serializer> coro relay(AsyncWriteStream& output, AsyncReadStream& input, - DynamicBuffer& buffer, Transform&& transform) { + DynamicBuffer& buffer, Parser& p, Serializer& sr) { static_assert(boost::beast::is_async_write_stream::value, "AsyncWriteStream requirements not met"); static_assert(boost::beast::is_async_read_stream::value, @@ -397,13 +397,6 @@ coro relay(AsyncWriteStream& output, AsyncReadStream& input, char* rbuf = _buf[0]; char* wbuf = _buf[1]; - parser p; - serializer sr{p.get()}; - - co_await async_read_header(input, buffer, p); - transform(p.get()); - co_await async_write_header(output, sr); - auto read = [&](char* buf) -> coro { if (p.is_done()) co_return 0; @@ -471,7 +464,21 @@ BOOST_AUTO_TEST_CASE(supports_buffer_body_with_socket) { auto work_guard = boost::asio::make_work_guard(ioc.get_executor()); auto thread = std::thread([&ioc] { ioc.run(); }); - co_spawn(ioc, relay(server_socket, server_socket, b, transform), + + parser p; + serializer sr{p.get()}; + + co_spawn( + ioc, + [&]() -> coro { + co_await async_read_header(server_socket, b, p); + transform(p.get()); + co_await async_write_header(server_socket, sr); + }, + boost::asio::use_future) + .get(); + + co_spawn(ioc, relay(server_socket, server_socket, b, p, sr), boost::asio::use_future) .get(); From b01913db8ad85db4cb4343f20c76f5b8545a896a Mon Sep 17 00:00:00 2001 From: Sungsik Date: Thu, 18 Sep 2025 16:35:13 +0200 Subject: [PATCH 17/35] nit --- test/unit/test_beast.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/unit/test_beast.cpp b/test/unit/test_beast.cpp index aedb3c25e..93c95d63c 100644 --- a/test/unit/test_beast.cpp +++ b/test/unit/test_beast.cpp @@ -398,9 +398,6 @@ coro relay(AsyncWriteStream& output, AsyncReadStream& input, char* wbuf = _buf[1]; auto read = [&](char* buf) -> coro { - if (p.is_done()) - co_return 0; - p.get().body().rdata = buf; p.get().body().rsize = buf_size; co_await ignore_need_buffer( From 949e27a612596c9f2575bf74defcd12a143661e0 Mon Sep 17 00:00:00 2001 From: Sungsik Date: Thu, 18 Sep 2025 16:49:33 +0200 Subject: [PATCH 18/35] Move relay implementation and double_buffer_body under proxy/cache --- src/proxy/cache/asio.h | 48 ++++++++++ .../proxy/cache}/double_buffer_body.h | 1 + test/unit/test_beast.cpp | 93 +------------------ 3 files changed, 53 insertions(+), 89 deletions(-) rename {test/unit => src/proxy/cache}/double_buffer_body.h (99%) diff --git a/src/proxy/cache/asio.h b/src/proxy/cache/asio.h index ebc5beb50..dab5d0eca 100644 --- a/src/proxy/cache/asio.h +++ b/src/proxy/cache/asio.h @@ -1,10 +1,12 @@ #pragma once +#include #include #include #include #include #include +#include using namespace boost::asio::experimental::awaitable_operators; @@ -99,4 +101,50 @@ template coro async_write(ep::http::stream& s, T& t) { } } +template coro ignore_need_buffer(Awaitable&& op) { + boost::system::error_code ec; + co_await op(boost::asio::redirect_error(boost::asio::use_awaitable, ec)); + if (ec && ec != boost::beast::http::error::need_buffer) { + throw boost::system::system_error(ec); + } +} + +template +coro relay(AsyncWriteStream& output, AsyncReadStream& input, + DynamicBuffer& buffer, Parser& p, Serializer& sr) { + static_assert(boost::beast::is_async_write_stream::value, + "AsyncWriteStream requirements not met"); + static_assert(boost::beast::is_async_read_stream::value, + "AsyncReadStream requirements not met"); + + constexpr std::size_t buf_size = 2_KiB; + char _buf[2][buf_size]; + char* rbuf = _buf[0]; + char* wbuf = _buf[1]; + + auto read = [&](char* buf) -> coro { + p.get().body().rdata = buf; + p.get().body().rsize = buf_size; + co_await ignore_need_buffer( + [&](auto token) { return async_read(input, buffer, p, token); }); + + co_return buf_size - p.get().body().rsize; + }; + + auto write = [&](char* buf, std::size_t size) -> coro { + p.get().body().more = size != 0; + p.get().body().wdata = buf; + p.get().body().wsize = size; + co_await ignore_need_buffer( + [&](auto token) { return async_write(output, sr, token); }); + }; + + for (auto bytes_read = co_await read(rbuf); + !p.is_done() || !sr.is_done();) { + std::swap(rbuf, wbuf); + bytes_read = co_await (read(rbuf) && write(wbuf, bytes_read)); + } +} + } // namespace uh::cluster::proxy::cache diff --git a/test/unit/double_buffer_body.h b/src/proxy/cache/double_buffer_body.h similarity index 99% rename from test/unit/double_buffer_body.h rename to src/proxy/cache/double_buffer_body.h index 201bce3cf..b43011d11 100644 --- a/test/unit/double_buffer_body.h +++ b/src/proxy/cache/double_buffer_body.h @@ -22,6 +22,7 @@ namespace http { of @ref buffer_body::value_type to appropriate values before each call to read or write during a stream operation. */ + struct double_buffer_body { /// The type of the body member when used in a message. struct value_type { diff --git a/test/unit/test_beast.cpp b/test/unit/test_beast.cpp index 93c95d63c..ca172a590 100644 --- a/test/unit/test_beast.cpp +++ b/test/unit/test_beast.cpp @@ -335,96 +335,11 @@ BOOST_AUTO_TEST_SUITE_END() } // namespace uh::cluster using namespace boost::beast::http; -#include "double_buffer_body.h" -#include -#include -#include -#include - -using namespace boost::asio::experimental::awaitable_operators; - -namespace uh::cluster { - -template coro ignore_need_buffer(Awaitable&& op) { - boost::system::error_code ec; - co_await op(boost::asio::redirect_error(boost::asio::use_awaitable, ec)); - if (ec && ec != boost::beast::http::error::need_buffer) { - throw boost::system::system_error(ec); - } -} - -/** Relay an HTTP message. - - This function efficiently relays an HTTP message from a downstream - client to an upstream server, or from an upstream server to a - downstream client. After the message header is read from the input, - a user provided transformation function is invoked which may change - the contents of the header before forwarding to the output. This may - be used to adjust fields such as Server, or proxy fields. - - @param output The stream to write to. - - @param input The stream to read from. - - @param buffer The buffer to use for the input. - - @param transform The header transformation to apply. The function will - be called with this signature: - @code - template - void transform(message< - isRequest, Body, Fields>&, // The message to transform - error_code&); // Set to the error, if any - @endcode - - @param ec Set to the error if any occurred. - - @tparam isRequest `true` to relay a request. - - @tparam Fields The type of fields to use for the message. -*/ -template -coro relay(AsyncWriteStream& output, AsyncReadStream& input, - DynamicBuffer& buffer, Parser& p, Serializer& sr) { - static_assert(boost::beast::is_async_write_stream::value, - "AsyncWriteStream requirements not met"); - static_assert(boost::beast::is_async_read_stream::value, - "AsyncReadStream requirements not met"); - - constexpr std::size_t buf_size = 2_KiB; - char _buf[2][buf_size]; - char* rbuf = _buf[0]; - char* wbuf = _buf[1]; - - auto read = [&](char* buf) -> coro { - p.get().body().rdata = buf; - p.get().body().rsize = buf_size; - co_await ignore_need_buffer( - [&](auto token) { return async_read(input, buffer, p, token); }); - - co_return buf_size - p.get().body().rsize; - }; - - auto write = [&](char* buf, std::size_t size) -> coro { - p.get().body().more = size != 0; - p.get().body().wdata = buf; - p.get().body().wsize = size; - co_await ignore_need_buffer( - [&](auto token) { return async_write(output, sr, token); }); - }; - - for (auto bytes_read = co_await read(rbuf); - !p.is_done() || !sr.is_done();) { - std::swap(rbuf, wbuf); - bytes_read = co_await (read(rbuf) && write(wbuf, bytes_read)); - } -} -} // namespace uh::cluster +#include #include -namespace uh::cluster { +namespace uh::cluster::proxy::cache { BOOST_AUTO_TEST_CASE(supports_buffer_body_with_socket) { using namespace boost::asio; using namespace boost::beast::http; @@ -441,7 +356,7 @@ BOOST_AUTO_TEST_CASE(supports_buffer_body_with_socket) { client_socket.connect(endpoint); server_thread.join(); - std::vector body(8 * 1024); + std::vector body(8_KiB + 17); std::mt19937 rng{std::random_device{}()}; std::uniform_int_distribution dist(0, 255); for (auto& c : body) @@ -497,4 +412,4 @@ BOOST_AUTO_TEST_CASE(supports_buffer_body_with_socket) { std::string_view received_body(&recv_buf[body_pos], n - body_pos); BOOST_TEST(received_body == std::string_view(body.data(), body.size())); } -} // namespace uh::cluster +} // namespace uh::cluster::proxy::cache From 26f9c605f9276bfcb8780375bb12c3bcf92f2257 Mon Sep 17 00:00:00 2001 From: Sungsik Date: Fri, 19 Sep 2025 12:47:21 +0200 Subject: [PATCH 19/35] Refactor cache body and finish relaying interleaving --- src/proxy/cache/asio.h | 218 +++++++++++++++++++++----- src/proxy/cache/disk/body.h | 91 ++++------- src/proxy/cache/disk/manager.h | 9 +- src/proxy/cache/double_buffer_body.h | 2 +- src/proxy/handler.cpp | 11 +- test/unit/test_beast.cpp | 80 ---------- test/unit/test_disk_cache_body.cpp | 159 ++++++++++++++++++- test/unit/test_disk_cache_manager.cpp | 22 +-- 8 files changed, 384 insertions(+), 208 deletions(-) diff --git a/src/proxy/cache/asio.h b/src/proxy/cache/asio.h index dab5d0eca..2cc8c5d6a 100644 --- a/src/proxy/cache/asio.h +++ b/src/proxy/cache/asio.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -14,12 +15,12 @@ namespace uh::cluster::proxy::cache { template concept ReaderBodyType = requires(T r, std::span sv) { - { r.put(sv) } -> std::same_as>; + { r.put(sv) } -> std::same_as>; }; template -concept WriterBodyType = requires(T w) { - { w.get() } -> std::same_as>>; +concept WriterBodyType = requires(T w, std::span sv) { + { w.get(sv) } -> std::same_as>>; }; template @@ -61,11 +62,7 @@ coro async_read(S& s, T& t, std::size_t size) { auto sv = co_await s.read(size); if (sv.empty()) break; - auto read = co_await reader.put(sv); - if (read != sv.size()) { - throw std::runtime_error( - "reader_body put() returned unexpected size"); - } + co_await reader.put(sv); co_await s.consume(); size -= sv.size(); } @@ -73,8 +70,12 @@ coro async_read(S& s, T& t, std::size_t size) { /* * It consumes automatically + * + * TODO: use socket instead of stream + * TODO: Choose which namespace we will use */ -template coro async_write(ep::http::stream& s, T& t) { +template +coro async_write(ep::http::stream& s, T& t) { auto&& writer = [&]() -> auto&& { if constexpr (BodyType) { return make_writer(t); @@ -86,18 +87,14 @@ template coro async_write(ep::http::stream& s, T& t) { } }(); - if constexpr (T::support_double_buffer::value) { - for (auto data = co_await writer.get(); !data.empty();) { - auto [d, _] = co_await (writer.get() && s.write(data)); - data = d; - } - } else { - while (true) { - auto data = co_await writer.get(); - if (data.empty()) - break; - co_await s.write(data); - } + char _buf[2][buf_size]; + char* rbuf = _buf[0]; + char* wbuf = _buf[1]; + + for (auto data = co_await writer.get({rbuf, buf_size}); !data.empty();) { + std::swap(rbuf, wbuf); + auto [d, _] = co_await (writer.get({rbuf, buf_size}) && s.write(data)); + data = d; } } @@ -109,10 +106,80 @@ template coro ignore_need_buffer(Awaitable&& op) { } } -template -coro relay(AsyncWriteStream& output, AsyncReadStream& input, - DynamicBuffer& buffer, Parser& p, Serializer& sr) { +template std::string serialize_header(Message& msg) { + using body_type = typename std::decay_t::body_type; + using fields_type = typename std::decay_t::fields_type; + constexpr bool is_request = std::decay_t::is_request::value; + boost::beast::http::serializer sr{msg}; + sr.split(true); + std::string header_str; + while (!sr.is_done()) { + auto const buf = sr.get(); + // buffers_to_string handles any buffer sequence or single buffer + header_str += boost::beast::buffers_to_string(buf); + sr.consume(boost::asio::buffer_size(buf)); + } + return header_str; +} + +template +coro async_write_store_header(ServerSocketType& server_socket, + Serializer& sr, SyncType& sync) { + std::ostringstream oss; + boost::system::error_code ec; + sr.split(true); + write_ostream(oss, sr, ec); + auto header_str = oss.str(); + co_await (sync.put(header_str) && [&]() -> coro { + co_await async_write(server_socket, boost::asio::buffer(header_str)); + }()); +} + +template +coro async_relay_body(AsyncWriteStream& output, AsyncReadStream& input, + DynamicBuffer& buffer, Parser& p, Serializer& sr) { + static_assert(boost::beast::is_async_write_stream::value, + "AsyncWriteStream requirements not met"); + static_assert(boost::beast::is_async_read_stream::value, + "AsyncReadStream requirements not met"); + + constexpr std::size_t buf_size = 2_KiB; + char _buf[2][buf_size]; + char* rbuf = _buf[0]; + char* wbuf = _buf[1]; + + auto read = [&](std::span sv) -> coro { + p.get().body().rdata = sv.data(); + p.get().body().rsize = sv.size(); + co_await ignore_need_buffer( + [&](auto token) { return async_read(input, buffer, p, token); }); + + co_return sv.size() - p.get().body().rsize; + }; + + auto write = [&](std::span sv) -> coro { + p.get().body().more = sv.size() != 0; + p.get().body().wdata = sv.data(); + p.get().body().wsize = sv.size(); + co_await ignore_need_buffer( + [&](auto token) { return async_write(output, sr, token); }); + }; + + for (auto bytes_read = co_await read({rbuf, buf_size}); + !p.is_done() || !sr.is_done();) { + std::swap(rbuf, wbuf); + bytes_read = + co_await (read({rbuf, buf_size}) && write({wbuf, bytes_read})); + } +} + +template +coro async_relay_store_body(AsyncWriteStream& output, + AsyncReadStream& input, DynamicBuffer& buffer, + Parser& p, Serializer& sr, + PayloadSync& sync) { static_assert(boost::beast::is_async_write_stream::value, "AsyncWriteStream requirements not met"); static_assert(boost::beast::is_async_read_stream::value, @@ -123,28 +190,109 @@ coro relay(AsyncWriteStream& output, AsyncReadStream& input, char* rbuf = _buf[0]; char* wbuf = _buf[1]; - auto read = [&](char* buf) -> coro { - p.get().body().rdata = buf; - p.get().body().rsize = buf_size; + auto read = [&](std::span sv) -> coro { + p.get().body().rdata = sv.data(); + p.get().body().rsize = sv.size(); co_await ignore_need_buffer( [&](auto token) { return async_read(input, buffer, p, token); }); - co_return buf_size - p.get().body().rsize; + co_return sv.size() - p.get().body().rsize; }; - auto write = [&](char* buf, std::size_t size) -> coro { - p.get().body().more = size != 0; - p.get().body().wdata = buf; - p.get().body().wsize = size; + auto write = [&](std::span sv) -> coro { + p.get().body().more = sv.size() != 0; + p.get().body().wdata = sv.data(); + p.get().body().wsize = sv.size(); co_await ignore_need_buffer( [&](auto token) { return async_write(output, sr, token); }); }; - for (auto bytes_read = co_await read(rbuf); + for (auto bytes_read = co_await read({rbuf, buf_size}); !p.is_done() || !sr.is_done();) { std::swap(rbuf, wbuf); - bytes_read = co_await (read(rbuf) && write(wbuf, bytes_read)); + bytes_read = + co_await ((read({rbuf, buf_size}) && write({wbuf, bytes_read})) && + sync.put({wbuf, bytes_read})); } } } // namespace uh::cluster::proxy::cache +namespace boost::beast::http { +// The detail namespace means "not public" +namespace detail { + +// This helper is needed for C++11. +// When invoked with a buffer sequence, writes the buffers `to the +// std::ostream`. +template class write_ostream_helper { + Serializer& sr_; + std::ostream& os_; + +public: + write_ostream_helper(Serializer& sr, std::ostream& os) + : sr_(sr), + os_(os) {} + + // This function is called by the serializer + template + void operator()(error_code& ec, ConstBufferSequence const& buffers) const { + // Error codes must be cleared on success + ec = {}; + + // Keep a running total of how much we wrote + std::size_t bytes_transferred = 0; + + // Loop over the buffer sequence + for (auto it = boost::asio::buffer_sequence_begin(buffers); + it != boost::asio::buffer_sequence_end(buffers); ++it) { + // This is the next buffer in the sequence + boost::asio::const_buffer const buffer = *it; + + // Write it to the std::ostream + os_.write(reinterpret_cast(buffer.data()), + buffer.size()); + + // If the std::ostream fails, convert it to an error code + if (os_.fail()) { + ec = make_error_code(errc::io_error); + return; + } + + // Adjust our running total + bytes_transferred += buffer_size(buffer); + } + + // Inform the serializer of the amount we consumed + sr_.consume(bytes_transferred); + } +}; + +} // namespace detail + +/** Write a message to a `std::ostream`. + + This function writes the serialized representation of the + HTTP/1 message to the sream. + + @param os The `std::ostream` to write to. + + @param msg The message to serialize. + + @param ec Set to the error, if any occurred. +*/ +template +void write_ostream(std::ostream& os, Serializer& sr, error_code& ec) { + + // This lambda is used as the "visit" function + detail::write_ostream_helper lambda{sr, os}; + do { + // In C++14 we could use a generic lambda but since we want + // to require only C++11, the lambda is written out by hand. + // This function call retrieves the next serialized buffers. + sr.next(ec, lambda); + if (ec) + return; + } while (!sr.is_done()); +} + +} // namespace boost::beast::http diff --git a/src/proxy/cache/disk/body.h b/src/proxy/cache/disk/body.h index 27fb3cdf2..0fdeb5d89 100644 --- a/src/proxy/cache/disk/body.h +++ b/src/proxy/cache/disk/body.h @@ -17,18 +17,21 @@ namespace uh::cluster::proxy::cache::disk { -class reader_body { +class writer { public: - reader_body(storage::data_view& writer) + writer(storage::data_view& writer) : m_storage{writer}, m_addr{} {} - coro put(std::span sv) { + template coro put(const T& s) { + return put(std::span(s.data(), s.size())); + } + + coro put(std::span sv) { auto addr = co_await m_storage.write(sv, {0}); m_hash.consume(sv); m_addr.append(addr); - co_return addr.data_size(); } /* @@ -48,47 +51,34 @@ class reader_body { address m_addr; }; -class writer_body { +class reader { public: - using support_double_buffer = std::false_type; - - writer_body(storage::data_view& storage, - std::shared_ptr objh, std::size_t buffer_size) + reader(storage::data_view& storage, std::shared_ptr objh) : m_storage(storage), - m_objh{std::move(objh)}, - m_buffer(buffer_size) {} - - writer_body(const writer_body&) = delete; - writer_body& operator=(const writer_body&) = delete; - writer_body(writer_body&&) = delete; - writer_body& operator=(writer_body&&) = delete; - - coro> get() { co_return co_await _get(&m_buffer); } - -private: - storage::data_view& m_storage; - std::shared_ptr m_objh; + m_objh{std::move(objh)} {} - std::size_t m_addr_index{0}; - std::size_t m_frag_offset{0}; + reader(const reader&) = delete; + reader& operator=(const reader&) = delete; + reader(reader&&) = delete; + reader& operator=(reader&&) = delete; -protected: - std::vector m_buffer; + template coro> get(T& s) { + return get(std::span(s.data(), s.size())); + } - coro> _get(std::vector* buffer) { + coro> get(std::span buffer) { std::size_t read_size = 0; - address partial_addr; while (m_addr_index < m_objh->get_address().size() && - read_size < buffer->size()) { + read_size < buffer.size()) { auto frag = m_objh->get_address().get(m_addr_index); if (m_frag_offset > 0) { frag.pointer += m_frag_offset; frag.size -= m_frag_offset; } - if (frag.size + read_size > buffer->size()) { - auto remains = buffer->size() - read_size; + if (frag.size + read_size > buffer.size()) { + auto remains = buffer.size() - read_size; m_frag_offset += remains; frag.size = remains; partial_addr.push(frag); @@ -103,40 +93,17 @@ class writer_body { if (read_size > 0) { co_await m_storage.read_address(partial_addr, - {buffer->data(), read_size}); + {buffer.data(), read_size}); } - co_return std::span{buffer->data(), read_size}; - } -}; - -class double_buffered_writer_body : private writer_body { -public: - using support_double_buffer = std::true_type; - - double_buffered_writer_body(storage::data_view& storage, - std::shared_ptr objh, - std::size_t buffer_size = 16_MiB) - : writer_body(storage, objh, buffer_size), - m_buffer2(buffer_size), - m_active(&m_buffer), - m_standby(&m_buffer2) {} - - double_buffered_writer_body(const double_buffered_writer_body&) = delete; - double_buffered_writer_body& - operator=(const double_buffered_writer_body&) = delete; - double_buffered_writer_body(double_buffered_writer_body&&) = delete; - double_buffered_writer_body& - operator=(double_buffered_writer_body&&) = delete; - - coro> get() { - auto rv = co_await writer_body::_get(m_active); - std::swap(m_active, m_standby); - co_return rv; + co_return std::span{buffer.data(), read_size}; } private: - std::vector m_buffer2; - std::vector* m_active; - std::vector* m_standby; + storage::data_view& m_storage; + std::shared_ptr m_objh; + + std::size_t m_addr_index{0}; + std::size_t m_frag_offset{0}; }; + } // namespace uh::cluster::proxy::cache::disk diff --git a/src/proxy/cache/disk/manager.h b/src/proxy/cache/disk/manager.h index cd9215a91..9365f2df2 100644 --- a/src/proxy/cache/disk/manager.h +++ b/src/proxy/cache/disk/manager.h @@ -32,8 +32,8 @@ class manager { * * It removed address information from the given body. */ - coro put(object_metadata key, reader_body& body) { - auto objh = body.get_object_handle(); + coro put(object_metadata key, writer& w) { + auto objh = w.get_object_handle(); auto obj_size = objh.data_size(); auto total_size = @@ -66,13 +66,12 @@ class manager { std::cout << "Total size after put: " << m_current_size << std::endl; } - std::unique_ptr get(object_metadata key) { + std::unique_ptr get(object_metadata key) { auto entry = m_cache->get(key); if (!entry) { return nullptr; } - return std::make_unique(m_storage, - std::move(entry)); + return std::make_unique(m_storage, std::move(entry)); } static manager create(boost::asio::io_context& ioc, data_view& storage, diff --git a/src/proxy/cache/double_buffer_body.h b/src/proxy/cache/double_buffer_body.h index b43011d11..5d0d4ba81 100644 --- a/src/proxy/cache/double_buffer_body.h +++ b/src/proxy/cache/double_buffer_body.h @@ -67,7 +67,7 @@ struct double_buffer_body { by `wdata` having `wsize` octets of valid storage as the next buffer representing the body. */ - void* wdata = nullptr; + const void* wdata = nullptr; /** The number of octets in the buffer pointed to by @ref wdata. diff --git a/src/proxy/handler.cpp b/src/proxy/handler.cpp index 96747f1c8..2251597ab 100644 --- a/src/proxy/handler.cpp +++ b/src/proxy/handler.cpp @@ -71,7 +71,7 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { LOG_INFO() << peer << ": done reading complete request"; - co_await cache::async_write(incoming, *wbody); + co_await cache::async_write<16_MiB>(incoming, *wbody); LOG_INFO() << peer << ": cache result served"; continue; @@ -123,20 +123,19 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { if (get_object::can_handle(*req)) { LOG_INFO() << peer << ": add " << buffer.size() << " response header"; - cache::disk::reader_body rbody(m_dv); - co_await rbody.put(buffer); + cache::disk::writer w(m_dv); + co_await w.put(buffer); - co_await cache::async_read(outgoing, rbody, len); + co_await cache::async_read(outgoing, w, len); co_await m_mgr.put( - cache::disk::object_metadata{req->object_key()}, rbody); + cache::disk::object_metadata{req->object_key()}, w); } else { std::size_t read = 0ull; while (read < len) { co_await outgoing.consume(); auto r = co_await outgoing.read(len - read); - // r: rbody read += r.size(); } diff --git a/test/unit/test_beast.cpp b/test/unit/test_beast.cpp index ca172a590..9c45406e2 100644 --- a/test/unit/test_beast.cpp +++ b/test/unit/test_beast.cpp @@ -333,83 +333,3 @@ BOOST_AUTO_TEST_CASE(supports_vector_of_const_buffers) { BOOST_AUTO_TEST_SUITE_END() } // namespace uh::cluster - -using namespace boost::beast::http; -#include - -#include - -namespace uh::cluster::proxy::cache { -BOOST_AUTO_TEST_CASE(supports_buffer_body_with_socket) { - using namespace boost::asio; - using namespace boost::beast::http; - - io_context ioc; - ip::tcp::acceptor acceptor(ioc, ip::tcp::endpoint(ip::tcp::v4(), 0)); - ip::tcp::endpoint endpoint = acceptor.local_endpoint(); - - ip::tcp::socket server_socket(ioc); - ip::tcp::socket client_socket(ioc); - - std::thread server_thread([&] { acceptor.accept(server_socket); }); - - client_socket.connect(endpoint); - server_thread.join(); - - std::vector body(8_KiB + 17); - std::mt19937 rng{std::random_device{}()}; - std::uniform_int_distribution dist(0, 255); - for (auto& c : body) - c = static_cast(dist(rng)); - - std::string request = "POST /upload HTTP/1.1\r\n" - "Host: example.com\r\n" - "User-Agent: test\r\n" - "Content-Length: " + - std::to_string(body.size()) + "\r\n\r\n"; - - write(client_socket, buffer(request)); - write(client_socket, buffer(body)); - - flat_buffer b; - auto transform = [](auto&) {}; - - auto work_guard = boost::asio::make_work_guard(ioc.get_executor()); - auto thread = std::thread([&ioc] { ioc.run(); }); - - parser p; - serializer sr{p.get()}; - - co_spawn( - ioc, - [&]() -> coro { - co_await async_read_header(server_socket, b, p); - transform(p.get()); - co_await async_write_header(server_socket, sr); - }, - boost::asio::use_future) - .get(); - - co_spawn(ioc, relay(server_socket, server_socket, b, p, sr), - boost::asio::use_future) - .get(); - - work_guard.reset(); - thread.join(); - - boost::system::error_code ec; - std::vector recv_buf(16 * 1024); - size_t n = client_socket.read_some(buffer(recv_buf), ec); - std::string output_str(recv_buf.data(), n); - - BOOST_CHECK_NE(output_str.find("POST /upload HTTP/1.1"), std::string::npos); - BOOST_CHECK_NE(output_str.find("Host: example.com"), std::string::npos); - BOOST_CHECK_NE(output_str.find("User-Agent: test"), std::string::npos); - - auto body_pos = output_str.find("\r\n\r\n"); - BOOST_REQUIRE(body_pos != std::string::npos); - body_pos += 4; - std::string_view received_body(&recv_buf[body_pos], n - body_pos); - BOOST_TEST(received_body == std::string_view(body.data(), body.size())); -} -} // namespace uh::cluster::proxy::cache diff --git a/test/unit/test_disk_cache_body.cpp b/test/unit/test_disk_cache_body.cpp index 5869491ed..fc85b0b71 100644 --- a/test/unit/test_disk_cache_body.cpp +++ b/test/unit/test_disk_cache_body.cpp @@ -13,6 +13,8 @@ #include #include +using namespace boost::beast::http; + namespace uh::cluster::proxy::cache::disk { BOOST_FIXTURE_TEST_SUITE(a_disk_cache_body, dedupe_fixture) @@ -69,13 +71,13 @@ BOOST_AUTO_TEST_CASE(supports_read) { BOOST_TEST(content_length == data.size()); // 6. Read body using async_read and reader_body - reader_body body(data_view); - boost::asio::co_spawn(m_ioc, async_read(stream, body, content_length), + writer w(data_view); + boost::asio::co_spawn(m_ioc, async_read(stream, w, content_length), boost::asio::use_future) .get(); - // 7. Verify body was stored and can be read back - auto objh = body.get_object_handle(); + // 7. Verify w was stored and can be read back + auto objh = w.get_object_handle(); BOOST_TEST(objh.data_size() == data.size()); std::vector buf(data.size()); @@ -106,7 +108,7 @@ BOOST_AUTO_TEST_CASE(supports_write) { auto objh = std::make_shared(std::move(addr)); BOOST_TEST(objh->data_size() == data.size()); - double_buffered_writer_body body(data_view, std::move(objh)); + reader r(data_view, std::move(objh)); // Set up TCP sockets boost::asio::ip::tcp::acceptor acceptor(m_ioc, @@ -124,8 +126,8 @@ BOOST_AUTO_TEST_CASE(supports_write) { boost::asio::write(client_sock, boost::asio::buffer(header)); BOOST_TEST(written_size == header.size()); - // Client writes body using async_write and writer_body - boost::asio::co_spawn(m_ioc, async_write(stream, body), + // Client writes r using async_write and writer_body + boost::asio::co_spawn(m_ioc, async_write<16_KiB>(stream, r), boost::asio::use_future) .get(); @@ -138,8 +140,149 @@ BOOST_AUTO_TEST_CASE(supports_write) { BOOST_TEST(received == expected_response); } -BOOST_AUTO_TEST_CASE(supports_write_using_smaller_buffer) {} +BOOST_AUTO_TEST_CASE(goes_with_relay_store_body) { + using namespace boost::asio; + using namespace boost::beast::http; + + ip::tcp::acceptor acceptor(m_ioc, ip::tcp::endpoint(ip::tcp::v4(), 0)); + ip::tcp::endpoint endpoint = acceptor.local_endpoint(); + + ip::tcp::socket server_socket(m_ioc); + ip::tcp::socket client_socket(m_ioc); + + std::thread server_thread([&] { acceptor.accept(server_socket); }); + + client_socket.connect(endpoint); + server_thread.join(); + + std::string body = random_string(8_KiB + 17); + std::string header = "POST /upload HTTP/1.1\r\n" + "Host: example.com\r\n" + "User-Agent: test\r\n" + "Content-Length: " + + std::to_string(body.size()) + "\r\n\r\n"; + auto raw_message = header + body; + + write(client_socket, buffer(header)); + write(client_socket, buffer(body)); + + boost::beast::flat_buffer b; + auto transform = [](auto&) {}; + + parser p; + serializer sr{p.get()}; + + writer w(data_view); + + co_spawn( + m_ioc, + [&]() -> coro { + co_await async_read_header(server_socket, b, p); + transform(p.get()); + co_await async_write_store_header(server_socket, sr, w); + co_await async_relay_store_body(server_socket, server_socket, b, p, + sr, w); + }, + boost::asio::use_future) + .get(); + + boost::system::error_code ec; + std::vector recv_buf(16 * 1024); + size_t n = client_socket.read_some(buffer(recv_buf), ec); + std::string output_str(recv_buf.data(), n); + + BOOST_CHECK_NE(output_str.find("POST /upload HTTP/1.1"), std::string::npos); + BOOST_CHECK_NE(output_str.find("Host: example.com"), std::string::npos); + BOOST_CHECK_NE(output_str.find("User-Agent: test"), std::string::npos); + + // auto body_pos = output_str.find("\r\n\r\n"); + // BOOST_REQUIRE(body_pos != std::string::npos); + // body_pos += 4; + // std::string_view received_body(&recv_buf[body_pos], n); + BOOST_TEST(output_str == + std::string_view(raw_message.data(), raw_message.size())); + + auto objh = w.get_object_handle(); + BOOST_TEST(objh.data_size() == raw_message.size()); + + std::vector buf(raw_message.size()); + boost::asio::co_spawn( + m_ioc, + data_view.read_address(objh.get_address(), + std::span{buf.data(), buf.size()}), + boost::asio::use_future) + .get(); + BOOST_TEST(std::string(buf.data(), buf.size()) == raw_message); +} BOOST_AUTO_TEST_SUITE_END() +BOOST_AUTO_TEST_CASE(test_relay_body) { + using namespace boost::asio; + using namespace boost::beast::http; + + io_context ioc; + ip::tcp::acceptor acceptor(ioc, ip::tcp::endpoint(ip::tcp::v4(), 0)); + ip::tcp::endpoint endpoint = acceptor.local_endpoint(); + + ip::tcp::socket server_socket(ioc); + ip::tcp::socket client_socket(ioc); + + std::thread server_thread([&] { acceptor.accept(server_socket); }); + + client_socket.connect(endpoint); + server_thread.join(); + + std::string body = random_string(8_KiB + 17); + std::string header = "POST /upload HTTP/1.1\r\n" + "Host: example.com\r\n" + "User-Agent: test\r\n" + "Content-Length: " + + std::to_string(body.size()) + "\r\n\r\n"; + + write(client_socket, buffer(header)); + write(client_socket, buffer(body)); + + boost::beast::flat_buffer b; + auto transform = [](auto&) {}; + + auto work_guard = boost::asio::make_work_guard(ioc.get_executor()); + auto thread = std::thread([&ioc] { ioc.run(); }); + + parser p; + serializer sr{p.get()}; + + co_spawn( + ioc, + [&]() -> coro { + co_await async_read_header(server_socket, b, p); + transform(p.get()); + co_await async_write_header(server_socket, sr); + }, + boost::asio::use_future) + .get(); + + co_spawn(ioc, async_relay_body(server_socket, server_socket, b, p, sr), + boost::asio::use_future) + .get(); + + work_guard.reset(); + thread.join(); + + boost::system::error_code ec; + std::vector recv_buf(16 * 1024); + size_t n = client_socket.read_some(buffer(recv_buf), ec); + std::string output_str(recv_buf.data(), n); + + BOOST_CHECK_NE(output_str.find("POST /upload HTTP/1.1"), std::string::npos); + BOOST_CHECK_NE(output_str.find("Host: example.com"), std::string::npos); + BOOST_CHECK_NE(output_str.find("User-Agent: test"), std::string::npos); + + auto body_pos = output_str.find("\r\n\r\n"); + BOOST_REQUIRE(body_pos != std::string::npos); + body_pos += 4; + std::string_view received_body(&recv_buf[body_pos], n - body_pos); + BOOST_TEST(received_body == std::string_view(body.data(), body.size())); +} + } // namespace uh::cluster::proxy::cache::disk diff --git a/test/unit/test_disk_cache_manager.cpp b/test/unit/test_disk_cache_manager.cpp index 30b53ce9b..a64a14a7c 100644 --- a/test/unit/test_disk_cache_manager.cpp +++ b/test/unit/test_disk_cache_manager.cpp @@ -15,10 +15,10 @@ BOOST_AUTO_TEST_CASE(put_and_get_with_metadata) { manager mgr{manager::create(m_ioc, data_view, 256)}; std::string data = random_string(64); - reader_body rbody(data_view); + writer w(data_view); boost::asio::co_spawn( - m_ioc, rbody.put(std::span(data.data(), data.size())), + m_ioc, w.put(std::span(data.data(), data.size())), boost::asio::use_future) .get(); @@ -26,18 +26,19 @@ BOOST_AUTO_TEST_CASE(put_and_get_with_metadata) { key.path = "/foo/bar"; key.version = "v1"; - boost::asio::co_spawn(m_ioc, mgr.put(key, rbody), boost::asio::use_future) + boost::asio::co_spawn(m_ioc, mgr.put(key, w), boost::asio::use_future) .get(); auto writer = mgr.get(key); BOOST_TEST(writer != nullptr); - auto buf = - boost::asio::co_spawn(m_ioc, writer->get(), boost::asio::use_future) + auto buf = std::string(128, '\0'); + auto sv = + boost::asio::co_spawn(m_ioc, writer->get(buf), boost::asio::use_future) .get(); - BOOST_TEST(buf.size() == data.size()); - BOOST_TEST(std::string(buf.data(), buf.size()) == data); + BOOST_TEST(sv.size() == data.size()); + BOOST_TEST(std::string(sv.data(), sv.size()) == data); } BOOST_AUTO_TEST_CASE(eviction_test) { @@ -50,9 +51,9 @@ BOOST_AUTO_TEST_CASE(eviction_test) { std::string data = random_string(32); datas.push_back(data); - reader_body rbody(data_view); + writer w(data_view); boost::asio::co_spawn( - m_ioc, rbody.put(std::span(data.data(), data.size())), + m_ioc, w.put(std::span(data.data(), data.size())), boost::asio::use_future) .get(); @@ -61,8 +62,7 @@ BOOST_AUTO_TEST_CASE(eviction_test) { key.version = "v" + std::to_string(i); keys.push_back(key); - boost::asio::co_spawn(m_ioc, mgr.put(key, rbody), - boost::asio::use_future) + boost::asio::co_spawn(m_ioc, mgr.put(key, w), boost::asio::use_future) .get(); } From e4b07388590724b43e142dfd31f0afcbc93a750b Mon Sep 17 00:00:00 2001 From: Sungsik Date: Fri, 19 Sep 2025 13:16:23 +0200 Subject: [PATCH 20/35] Save header length in object_handle --- src/proxy/cache/asio.h | 25 +++++++++++++++++-------- src/proxy/cache/disk/body.h | 4 +++- src/proxy/cache/disk/object.h | 8 ++++++-- test/unit/test_disk_cache_body.cpp | 7 +++---- 4 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/proxy/cache/asio.h b/src/proxy/cache/asio.h index 2cc8c5d6a..1950e96be 100644 --- a/src/proxy/cache/asio.h +++ b/src/proxy/cache/asio.h @@ -123,8 +123,8 @@ template std::string serialize_header(Message& msg) { } template -coro async_write_store_header(ServerSocketType& server_socket, - Serializer& sr, SyncType& sync) { +coro async_write_store_header(ServerSocketType& server_socket, + Serializer& sr, SyncType& sync) { std::ostringstream oss; boost::system::error_code ec; sr.split(true); @@ -133,12 +133,15 @@ coro async_write_store_header(ServerSocketType& server_socket, co_await (sync.put(header_str) && [&]() -> coro { co_await async_write(server_socket, boost::asio::buffer(header_str)); }()); + sync.set_header_size(header_str.size()); + co_return header_str.size(); } template -coro async_relay_body(AsyncWriteStream& output, AsyncReadStream& input, - DynamicBuffer& buffer, Parser& p, Serializer& sr) { +coro +async_relay_body(AsyncWriteStream& output, AsyncReadStream& input, + DynamicBuffer& buffer, Parser& p, Serializer& sr) { static_assert(boost::beast::is_async_write_stream::value, "AsyncWriteStream requirements not met"); static_assert(boost::beast::is_async_read_stream::value, @@ -166,20 +169,23 @@ coro async_relay_body(AsyncWriteStream& output, AsyncReadStream& input, [&](auto token) { return async_write(output, sr, token); }); }; + std::size_t total_bytes = 0; for (auto bytes_read = co_await read({rbuf, buf_size}); !p.is_done() || !sr.is_done();) { std::swap(rbuf, wbuf); bytes_read = co_await (read({rbuf, buf_size}) && write({wbuf, bytes_read})); + total_bytes += bytes_read; } + co_return total_bytes; } template -coro async_relay_store_body(AsyncWriteStream& output, - AsyncReadStream& input, DynamicBuffer& buffer, - Parser& p, Serializer& sr, - PayloadSync& sync) { +coro async_relay_store_body(AsyncWriteStream& output, + AsyncReadStream& input, + DynamicBuffer& buffer, Parser& p, + Serializer& sr, PayloadSync& sync) { static_assert(boost::beast::is_async_write_stream::value, "AsyncWriteStream requirements not met"); static_assert(boost::beast::is_async_read_stream::value, @@ -207,13 +213,16 @@ coro async_relay_store_body(AsyncWriteStream& output, [&](auto token) { return async_write(output, sr, token); }); }; + std::size_t total_bytes = 0; for (auto bytes_read = co_await read({rbuf, buf_size}); !p.is_done() || !sr.is_done();) { std::swap(rbuf, wbuf); bytes_read = co_await ((read({rbuf, buf_size}) && write({wbuf, bytes_read})) && sync.put({wbuf, bytes_read})); + total_bytes += bytes_read; } + co_return total_bytes; } } // namespace uh::cluster::proxy::cache diff --git a/src/proxy/cache/disk/body.h b/src/proxy/cache/disk/body.h index 0fdeb5d89..97b7046e7 100644 --- a/src/proxy/cache/disk/body.h +++ b/src/proxy/cache/disk/body.h @@ -34,6 +34,7 @@ class writer { m_addr.append(addr); } + void set_header_size(std::size_t size) { m_header_size = size; } /* * Moves and returns the internal resource. * May only be called once; further calls will return an empty or invalid @@ -41,7 +42,7 @@ class writer { */ object_handle get_object_handle() { // TODO: set etag with `to_hex(m_hash.finalize())` - return object_handle(std::move(m_addr)); + return object_handle(std::move(m_addr), m_header_size); } private: @@ -49,6 +50,7 @@ class writer { md5 m_hash; address m_addr; + std::size_t m_header_size{0}; }; class reader { diff --git a/src/proxy/cache/disk/object.h b/src/proxy/cache/disk/object.h index 79e75ab93..f7f054fec 100644 --- a/src/proxy/cache/disk/object.h +++ b/src/proxy/cache/disk/object.h @@ -37,18 +37,22 @@ namespace uh::cluster::proxy::cache::disk { struct object_handle { object_handle() = default; - object_handle(address&& a) - : m_addr(std::move(a)) {} + object_handle(address&& a, std::size_t header_size = 0) + : m_addr(std::move(a)), + m_header_size(header_size) {} object_handle(object_handle&&) = default; object_handle& operator=(object_handle&&) = default; + std::size_t header_size() const { return m_header_size; } + std::size_t data_size() const { return m_addr.data_size(); } const address& get_address() const { return m_addr; } private: address m_addr; + std::size_t m_header_size; }; } // namespace uh::cluster::proxy::cache::disk diff --git a/test/unit/test_disk_cache_body.cpp b/test/unit/test_disk_cache_body.cpp index fc85b0b71..27eef3972 100644 --- a/test/unit/test_disk_cache_body.cpp +++ b/test/unit/test_disk_cache_body.cpp @@ -167,7 +167,6 @@ BOOST_AUTO_TEST_CASE(goes_with_relay_store_body) { write(client_socket, buffer(body)); boost::beast::flat_buffer b; - auto transform = [](auto&) {}; parser p; serializer sr{p.get()}; @@ -177,9 +176,9 @@ BOOST_AUTO_TEST_CASE(goes_with_relay_store_body) { co_spawn( m_ioc, [&]() -> coro { - co_await async_read_header(server_socket, b, p); - transform(p.get()); - co_await async_write_store_header(server_socket, sr, w); + auto n = co_await async_read_header(server_socket, b, p); + auto m = co_await async_write_store_header(server_socket, sr, w); + BOOST_TEST(n == m); co_await async_relay_store_body(server_socket, server_socket, b, p, sr, w); }, From 1676cad84a572bb13b0598aba1c762662df60c9e Mon Sep 17 00:00:00 2001 From: Sungsik Date: Fri, 19 Sep 2025 15:43:30 +0200 Subject: [PATCH 21/35] Relaying response of first get request: done --- src/proxy/cache/asio.h | 46 +++++++------ src/proxy/cache/disk/body.h | 6 +- src/proxy/handler.cpp | 105 +++++++++++++++-------------- test/unit/test_disk_cache_body.cpp | 7 +- 4 files changed, 89 insertions(+), 75 deletions(-) diff --git a/src/proxy/cache/asio.h b/src/proxy/cache/asio.h index 1950e96be..5898c7feb 100644 --- a/src/proxy/cache/asio.h +++ b/src/proxy/cache/asio.h @@ -74,7 +74,7 @@ coro async_read(S& s, T& t, std::size_t size) { * TODO: use socket instead of stream * TODO: Choose which namespace we will use */ -template +template coro async_write(ep::http::stream& s, T& t) { auto&& writer = [&]() -> auto&& { if constexpr (BodyType) { @@ -87,13 +87,14 @@ coro async_write(ep::http::stream& s, T& t) { } }(); - char _buf[2][buf_size]; + char _buf[2][buffer_size]; char* rbuf = _buf[0]; char* wbuf = _buf[1]; - for (auto data = co_await writer.get({rbuf, buf_size}); !data.empty();) { + for (auto data = co_await writer.get({rbuf, buffer_size}); !data.empty();) { std::swap(rbuf, wbuf); - auto [d, _] = co_await (writer.get({rbuf, buf_size}) && s.write(data)); + auto [d, _] = + co_await (writer.get({rbuf, buffer_size}) && s.write(data)); data = d; } } @@ -130,6 +131,9 @@ coro async_write_store_header(ServerSocketType& server_socket, sr.split(true); write_ostream(oss, sr, ec); auto header_str = oss.str(); + if (header_str.size() == 0) { + throw std::runtime_error("Could not serialize header"); + } co_await (sync.put(header_str) && [&]() -> coro { co_await async_write(server_socket, boost::asio::buffer(header_str)); }()); @@ -137,18 +141,18 @@ coro async_write_store_header(ServerSocketType& server_socket, co_return header_str.size(); } -template +template coro -async_relay_body(AsyncWriteStream& output, AsyncReadStream& input, +async_relay_body(AsyncReadStream& input, AsyncWriteStream& output, DynamicBuffer& buffer, Parser& p, Serializer& sr) { static_assert(boost::beast::is_async_write_stream::value, "AsyncWriteStream requirements not met"); static_assert(boost::beast::is_async_read_stream::value, "AsyncReadStream requirements not met"); - constexpr std::size_t buf_size = 2_KiB; - char _buf[2][buf_size]; + char _buf[2][buffer_size]; char* rbuf = _buf[0]; char* wbuf = _buf[1]; @@ -170,20 +174,21 @@ async_relay_body(AsyncWriteStream& output, AsyncReadStream& input, }; std::size_t total_bytes = 0; - for (auto bytes_read = co_await read({rbuf, buf_size}); + for (auto bytes_read = co_await read({rbuf, buffer_size}); !p.is_done() || !sr.is_done();) { std::swap(rbuf, wbuf); bytes_read = - co_await (read({rbuf, buf_size}) && write({wbuf, bytes_read})); + co_await (read({rbuf, buffer_size}) && write({wbuf, bytes_read})); total_bytes += bytes_read; } co_return total_bytes; } -template -coro async_relay_store_body(AsyncWriteStream& output, - AsyncReadStream& input, +template +coro async_relay_store_body(AsyncReadStream& input, + AsyncWriteStream& output, DynamicBuffer& buffer, Parser& p, Serializer& sr, PayloadSync& sync) { static_assert(boost::beast::is_async_write_stream::value, @@ -191,8 +196,7 @@ coro async_relay_store_body(AsyncWriteStream& output, static_assert(boost::beast::is_async_read_stream::value, "AsyncReadStream requirements not met"); - constexpr std::size_t buf_size = 2_KiB; - char _buf[2][buf_size]; + char _buf[2][buffer_size]; char* rbuf = _buf[0]; char* wbuf = _buf[1]; @@ -214,12 +218,12 @@ coro async_relay_store_body(AsyncWriteStream& output, }; std::size_t total_bytes = 0; - for (auto bytes_read = co_await read({rbuf, buf_size}); + for (auto bytes_read = co_await read({rbuf, buffer_size}); !p.is_done() || !sr.is_done();) { std::swap(rbuf, wbuf); - bytes_read = - co_await ((read({rbuf, buf_size}) && write({wbuf, bytes_read})) && - sync.put({wbuf, bytes_read})); + bytes_read = co_await ( + (read({rbuf, buffer_size}) && write({wbuf, bytes_read})) && + sync.put({wbuf, bytes_read})); total_bytes += bytes_read; } co_return total_bytes; diff --git a/src/proxy/cache/disk/body.h b/src/proxy/cache/disk/body.h index 97b7046e7..717af7533 100644 --- a/src/proxy/cache/disk/body.h +++ b/src/proxy/cache/disk/body.h @@ -28,7 +28,9 @@ class writer { } coro put(std::span sv) { - + if (sv.size() == 0) { + co_return; + } auto addr = co_await m_storage.write(sv, {0}); m_hash.consume(sv); m_addr.append(addr); @@ -64,6 +66,8 @@ class reader { reader(reader&&) = delete; reader& operator=(reader&&) = delete; + std::size_t get_header_size() const { return m_objh->header_size(); } + template coro> get(T& s) { return get(std::span(s.data(), s.size())); } diff --git a/src/proxy/handler.cpp b/src/proxy/handler.cpp index 2251597ab..5b92f8e01 100644 --- a/src/proxy/handler.cpp +++ b/src/proxy/handler.cpp @@ -10,7 +10,6 @@ #include using namespace uh::cluster::ep::http; - namespace uh::cluster::proxy { handler::handler( @@ -28,7 +27,8 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { auto peer = s.remote_endpoint(); forward_stream incoming(s, *ds); - forward_stream outgoing(*ds, s); + auto& outgoing{*ds}; + boost::beast::flat_buffer o_buffer; for (;;) { /* @@ -47,19 +47,17 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { << " " << r.target(); incoming.set_mode(forward_stream::forwarding); - outgoing.set_mode(forward_stream::forwarding); std::unique_ptr req = co_await m_factory->create(incoming, rawreq); if (get_object::can_handle(*req)) { - auto wbody = + auto reader = m_mgr.get(cache::disk::object_metadata{req->object_key()}); - if (wbody) { + if (reader) { LOG_INFO() << peer << ": handling from cache"; incoming.set_mode(forward_stream::deleting); - outgoing.set_mode(forward_stream::deleting); - // forwarding request + // TODO: forwarding request auto& b = req->body(); auto bs = b.buffer_size(); @@ -70,8 +68,10 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { co_await b.consume(); LOG_INFO() << peer << ": done reading complete request"; + auto header = std::vector(reader->get_header_size()); - co_await cache::async_write<16_MiB>(incoming, *wbody); + co_await async_write(s, co_await reader->get(header)); + co_await cache::async_write<16_MiB>(incoming, *reader); LOG_INFO() << peer << ": cache result served"; continue; @@ -85,11 +85,19 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { expect && *expect == "100-continue") { LOG_INFO() << req->peer() << ": forwarding 100 CONTINUE"; // TODO timeout - co_await outgoing.read_until("\r\n\r\n"); - co_await outgoing.consume(); + boost::beast::http::parser + p; + boost::beast::http::serializer + sr{p.get()}; + co_await boost::beast::http::async_read_header(outgoing, + o_buffer, p); + co_await async_write_header(s, sr); } - // forwarding request + // forwarding request body auto& b = req->body(); auto bs = b.buffer_size(); @@ -100,50 +108,47 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { co_await b.consume(); // forwarding response - beast::http::response_parser parser; - parser.body_limit((std::numeric_limits::max)()); - - auto buffer = co_await outgoing.read_until("\r\n\r\n"); - - beast::error_code ec; - parser.put(boost::asio::buffer(buffer), ec); - - auto res = parser.release(); - - bs = outgoing.buffer_size(); - std::size_t len = std::stoul(res.at("Content-Length")); - if (r.method() == boost::beast::http::verb::head && - (res.result_int() / 100 == 2)) { - len = 0; - } - - LOG_INFO() << peer << ": sending response " << res.result_int() - << " " << res.reason() << " -- " << len; - - if (get_object::can_handle(*req)) { - LOG_INFO() << peer << ": add " << buffer.size() - << " response header"; - cache::disk::writer w(m_dv); - co_await w.put(buffer); - - co_await cache::async_read(outgoing, w, len); - + // TODO: alias default parser and serializer for relaying + boost::beast::http::parser + p; + p.body_limit(std::numeric_limits::max()); + boost::beast::http::serializer< + false, boost::beast::http::double_buffer_body, + boost::beast::http::fields> + sr{p.get()}; + + LOG_INFO() << peer << ": reading header from downstream"; + co_await boost::beast::http::async_read_header(outgoing, o_buffer, + p); + // co_await async_write_header(s, sr); + + if (r.method() == boost::beast::http::verb::head) { + LOG_INFO() << peer << ": HEAD request, skipping body relay"; + sr.split(true); + co_await boost::beast::http::async_write_header(s, sr); + + } else if (get_object::can_handle(*req)) { + cache::disk::writer writer(m_dv); + LOG_INFO() << peer << ": writing header to client and cache"; + co_await cache::async_write_store_header(s, sr, writer); + LOG_INFO() << peer << ": writing body to client and cache"; + co_await cache::async_relay_store_body<16_MiB>( + outgoing, s, o_buffer, p, sr, writer); + LOG_INFO() << peer << ": storing object to cache"; co_await m_mgr.put( - cache::disk::object_metadata{req->object_key()}, w); - } else { - std::size_t read = 0ull; - while (read < len) { - co_await outgoing.consume(); + cache::disk::object_metadata{req->object_key()}, writer); - auto r = co_await outgoing.read(len - read); - read += r.size(); - } - - co_await outgoing.consume(); + } else { + LOG_INFO() << peer << ": relaying header to client"; + co_await boost::beast::http::async_write_header(s, sr); + LOG_INFO() << peer << ": relaying body to client"; + co_await cache::async_relay_body<4_KiB>(outgoing, s, o_buffer, + p, sr); } + LOG_INFO() << peer << ": done"; metric::increase(1); - } catch (const boost::system::system_error& e) { throw; } catch (const command_exception& e) { diff --git a/test/unit/test_disk_cache_body.cpp b/test/unit/test_disk_cache_body.cpp index 27eef3972..5d22f5ba9 100644 --- a/test/unit/test_disk_cache_body.cpp +++ b/test/unit/test_disk_cache_body.cpp @@ -179,8 +179,8 @@ BOOST_AUTO_TEST_CASE(goes_with_relay_store_body) { auto n = co_await async_read_header(server_socket, b, p); auto m = co_await async_write_store_header(server_socket, sr, w); BOOST_TEST(n == m); - co_await async_relay_store_body(server_socket, server_socket, b, p, - sr, w); + co_await async_relay_store_body<2_KiB>(server_socket, server_socket, + b, p, sr, w); }, boost::asio::use_future) .get(); @@ -261,7 +261,8 @@ BOOST_AUTO_TEST_CASE(test_relay_body) { boost::asio::use_future) .get(); - co_spawn(ioc, async_relay_body(server_socket, server_socket, b, p, sr), + co_spawn(ioc, + async_relay_body<2_KiB>(server_socket, server_socket, b, p, sr), boost::asio::use_future) .get(); From 0fc0174b887895e9e24a6d69beabca56b242526b Mon Sep 17 00:00:00 2001 From: Sungsik Date: Fri, 19 Sep 2025 20:58:42 +0200 Subject: [PATCH 22/35] [tmp] use group rather than && --- src/proxy/cache/asio.h | 6 ++--- src/proxy/cache/awaitable_operators.h | 34 +++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/proxy/cache/asio.h b/src/proxy/cache/asio.h index 5898c7feb..49f590f12 100644 --- a/src/proxy/cache/asio.h +++ b/src/proxy/cache/asio.h @@ -221,9 +221,9 @@ coro async_relay_store_body(AsyncReadStream& input, for (auto bytes_read = co_await read({rbuf, buffer_size}); !p.is_done() || !sr.is_done();) { std::swap(rbuf, wbuf); - bytes_read = co_await ( - (read({rbuf, buffer_size}) && write({wbuf, bytes_read})) && - sync.put({wbuf, bytes_read})); + bytes_read = + co_await group(read({rbuf, buffer_size}), write({wbuf, bytes_read}), + sync.put({wbuf, bytes_read})); total_bytes += bytes_read; } co_return total_bytes; diff --git a/src/proxy/cache/awaitable_operators.h b/src/proxy/cache/awaitable_operators.h index fbc7e91f6..27d77252e 100644 --- a/src/proxy/cache/awaitable_operators.h +++ b/src/proxy/cache/awaitable_operators.h @@ -487,6 +487,40 @@ operator||(traced_awaitable, Executor> t, } } +template +traced_awaitable group(traced_awaitable t, + traced_awaitable u1, + traced_awaitable u2) { + auto ex = co_await this_coro::executor; + auto context = co_await this_coro::context; + + auto [order, ex0, r0, ex1, ex2] = + co_await make_parallel_group( + co_spawn( + ex, + detail::awaitable_wrap(std::move(t.continue_trace(context))), + deferred), + co_spawn(ex, std::move(u1.continue_trace(context)), deferred), + co_spawn(ex, std::move(u2.continue_trace(context)), deferred)) + .async_wait(wait_for_one_error(), deferred); + + int exception_count = (ex0 ? 1 : 0) + (ex1 ? 1 : 0) + (ex2 ? 1 : 0); + if (exception_count > 1) + throw multiple_exceptions(ex0 ? ex0 : (ex1 ? ex1 : ex2)); + if (ex0) + std::rethrow_exception(ex0); + if (ex1) + std::rethrow_exception(ex1); + if (ex2) + std::rethrow_exception(ex2); + + if constexpr (std::is_void_v) { + co_return; + } else { + co_return std::move(detail::awaitable_unwrap(r0)); + } +} + } // namespace awaitable_operators } // namespace experimental } // namespace asio From 6784dd03ed595ee625d5d5e6ec0dfd558fb56e36 Mon Sep 17 00:00:00 2001 From: Sungsik Date: Sat, 20 Sep 2025 14:56:11 +0200 Subject: [PATCH 23/35] Revert "[tmp] use group rather than &&" This reverts commit 0fc0174b887895e9e24a6d69beabca56b242526b. --- src/proxy/cache/asio.h | 6 ++--- src/proxy/cache/awaitable_operators.h | 34 --------------------------- 2 files changed, 3 insertions(+), 37 deletions(-) diff --git a/src/proxy/cache/asio.h b/src/proxy/cache/asio.h index 49f590f12..5898c7feb 100644 --- a/src/proxy/cache/asio.h +++ b/src/proxy/cache/asio.h @@ -221,9 +221,9 @@ coro async_relay_store_body(AsyncReadStream& input, for (auto bytes_read = co_await read({rbuf, buffer_size}); !p.is_done() || !sr.is_done();) { std::swap(rbuf, wbuf); - bytes_read = - co_await group(read({rbuf, buffer_size}), write({wbuf, bytes_read}), - sync.put({wbuf, bytes_read})); + bytes_read = co_await ( + (read({rbuf, buffer_size}) && write({wbuf, bytes_read})) && + sync.put({wbuf, bytes_read})); total_bytes += bytes_read; } co_return total_bytes; diff --git a/src/proxy/cache/awaitable_operators.h b/src/proxy/cache/awaitable_operators.h index 27d77252e..fbc7e91f6 100644 --- a/src/proxy/cache/awaitable_operators.h +++ b/src/proxy/cache/awaitable_operators.h @@ -487,40 +487,6 @@ operator||(traced_awaitable, Executor> t, } } -template -traced_awaitable group(traced_awaitable t, - traced_awaitable u1, - traced_awaitable u2) { - auto ex = co_await this_coro::executor; - auto context = co_await this_coro::context; - - auto [order, ex0, r0, ex1, ex2] = - co_await make_parallel_group( - co_spawn( - ex, - detail::awaitable_wrap(std::move(t.continue_trace(context))), - deferred), - co_spawn(ex, std::move(u1.continue_trace(context)), deferred), - co_spawn(ex, std::move(u2.continue_trace(context)), deferred)) - .async_wait(wait_for_one_error(), deferred); - - int exception_count = (ex0 ? 1 : 0) + (ex1 ? 1 : 0) + (ex2 ? 1 : 0); - if (exception_count > 1) - throw multiple_exceptions(ex0 ? ex0 : (ex1 ? ex1 : ex2)); - if (ex0) - std::rethrow_exception(ex0); - if (ex1) - std::rethrow_exception(ex1); - if (ex2) - std::rethrow_exception(ex2); - - if constexpr (std::is_void_v) { - co_return; - } else { - co_return std::move(detail::awaitable_unwrap(r0)); - } -} - } // namespace awaitable_operators } // namespace experimental } // namespace asio From 0777daa43cd2aaf015a4569d830792ec9ba2deed Mon Sep 17 00:00:00 2001 From: Sungsik Date: Mon, 22 Sep 2025 14:26:15 +0200 Subject: [PATCH 24/35] Pipelining first get request, without copying buffers --- src/proxy/cache/asio.h | 138 +++++++++++++++++++++-------- src/proxy/handler.cpp | 19 ++-- test/unit/test_disk_cache_body.cpp | 19 ++-- 3 files changed, 124 insertions(+), 52 deletions(-) diff --git a/src/proxy/cache/asio.h b/src/proxy/cache/asio.h index 5898c7feb..e59c6d20f 100644 --- a/src/proxy/cache/asio.h +++ b/src/proxy/cache/asio.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -9,6 +10,8 @@ #include #include +#include + using namespace boost::asio::experimental::awaitable_operators; namespace uh::cluster::proxy::cache { @@ -68,6 +71,14 @@ coro async_read(S& s, T& t, std::size_t size) { } } +inline std::span get_span(boost::asio::const_buffer buffer) { + return {static_cast(buffer.data()), buffer.size()}; +} + +inline std::span get_span(boost::asio::mutable_buffer buffer) { + return {static_cast(buffer.data()), buffer.size()}; +} + /* * It consumes automatically * @@ -141,47 +152,100 @@ coro async_write_store_header(ServerSocketType& server_socket, co_return header_str.size(); } -template -coro -async_relay_body(AsyncReadStream& input, AsyncWriteStream& output, - DynamicBuffer& buffer, Parser& p, Serializer& sr) { - static_assert(boost::beast::is_async_write_stream::value, - "AsyncWriteStream requirements not met"); - static_assert(boost::beast::is_async_read_stream::value, - "AsyncReadStream requirements not met"); - - char _buf[2][buffer_size]; - char* rbuf = _buf[0]; - char* wbuf = _buf[1]; - - auto read = [&](std::span sv) -> coro { - p.get().body().rdata = sv.data(); - p.get().body().rsize = sv.size(); - co_await ignore_need_buffer( - [&](auto token) { return async_read(input, buffer, p, token); }); +template +coro async_relay_body(Incomming& in, Outgoing& out, + boost::beast::flat_buffer& b, + std::size_t payload_size) { + + if (payload_size > chunk_size) { + boost::beast::flat_buffer b2; + auto* rbuf = &b; + auto* wbuf = &b2; + + for (auto n = co_await async_read( + in, rbuf->prepare(std::min(payload_size, chunk_size) - + rbuf->data().size())); + n != 0;) { + std::swap(rbuf, wbuf); + wbuf->commit(n + wbuf->data().size()); + payload_size -= wbuf->data().size(); + auto new_n = co_await ( // + [&]() -> coro { + co_return co_await async_read( + in, rbuf->prepare(std::min(payload_size, chunk_size))); + }() && [&]() -> coro { + co_await async_write(out, get_span(wbuf->data())); + }()); + wbuf->consume(wbuf->data().size()); + n = new_n; + } + } else { + auto n = + co_await async_read(in, b.prepare(payload_size - b.data().size())); + b.commit(n + b.data().size()); + co_await async_write(out, get_span(b.data())); + b.consume(b.data().size()); + } - co_return sv.size() - p.get().body().rsize; - }; + b.shrink_to_fit(); +} - auto write = [&](std::span sv) -> coro { - p.get().body().more = sv.size() != 0; - p.get().body().wdata = sv.data(); - p.get().body().wsize = sv.size(); - co_await ignore_need_buffer( - [&](auto token) { return async_write(output, sr, token); }); - }; +template +std::optional get_content_length(const Message& msg) { + auto it = msg.find(boost::beast::http::field::content_length); + if (it != msg.end()) { + try { + return std::stoull( + std::string(it->value().data(), it->value().size())); + } catch (...) { + return std::nullopt; + } + } + return std::nullopt; +} - std::size_t total_bytes = 0; - for (auto bytes_read = co_await read({rbuf, buffer_size}); - !p.is_done() || !sr.is_done();) { - std::swap(rbuf, wbuf); - bytes_read = - co_await (read({rbuf, buffer_size}) && write({wbuf, bytes_read})); - total_bytes += bytes_read; +template +coro async_relay_store_body(Incomming& in, Outgoing& out, + boost::beast::flat_buffer& b, + PayloadWriter& writer, + std::size_t payload_size) { + + if (payload_size > chunk_size) { + boost::beast::flat_buffer b2; + auto* rbuf = &b; + auto* wbuf = &b2; + + for (auto n = co_await async_read( + in, rbuf->prepare(std::min(payload_size, chunk_size) - + rbuf->data().size())); + n != 0;) { + std::swap(rbuf, wbuf); + wbuf->commit(n + wbuf->data().size()); + payload_size -= wbuf->data().size(); + auto new_n = co_await ( // + [&]() -> coro { + co_return co_await async_read( + in, rbuf->prepare(std::min(payload_size, chunk_size))); + }() && ([&]() -> coro { + co_await async_write(out, get_span(wbuf->data())); + }() && writer.put(get_span(wbuf->data())) // + ) // + ); + wbuf->consume(wbuf->data().size()); + n = new_n; + } + } else { + auto n = + co_await async_read(in, b.prepare(payload_size - b.data().size())); + b.commit(n + b.data().size()); + co_await ([&]() -> coro { + co_await async_write(out, get_span(b.data())); + }() && writer.put(get_span(b.data()))); + b.consume(b.data().size()); } - co_return total_bytes; + + b.shrink_to_fit(); } template handler::handle(boost::asio::ip::tcp::socket s) { } else if (get_object::can_handle(*req)) { cache::disk::writer writer(m_dv); - LOG_INFO() << peer << ": writing header to client and cache"; co_await cache::async_write_store_header(s, sr, writer); - LOG_INFO() << peer << ": writing body to client and cache"; - co_await cache::async_relay_store_body<16_MiB>( - outgoing, s, o_buffer, p, sr, writer); - LOG_INFO() << peer << ": storing object to cache"; + auto body_size = cache::get_content_length(p.get()); + if (!body_size.has_value()) { + throw std::runtime_error("no content length"); + } + co_await cache::async_relay_store_body<32_MiB>( + outgoing, s, o_buffer, writer, *body_size); co_await m_mgr.put( cache::disk::object_metadata{req->object_key()}, writer); } else { - LOG_INFO() << peer << ": relaying header to client"; co_await boost::beast::http::async_write_header(s, sr); - LOG_INFO() << peer << ": relaying body to client"; + auto body_size = cache::get_content_length(p.get()); + if (!body_size.has_value()) { + throw std::runtime_error("no content length"); + } co_await cache::async_relay_body<4_KiB>(outgoing, s, o_buffer, - p, sr); + *body_size); } LOG_INFO() << peer << ": done"; diff --git a/test/unit/test_disk_cache_body.cpp b/test/unit/test_disk_cache_body.cpp index 5d22f5ba9..692481a43 100644 --- a/test/unit/test_disk_cache_body.cpp +++ b/test/unit/test_disk_cache_body.cpp @@ -179,8 +179,12 @@ BOOST_AUTO_TEST_CASE(goes_with_relay_store_body) { auto n = co_await async_read_header(server_socket, b, p); auto m = co_await async_write_store_header(server_socket, sr, w); BOOST_TEST(n == m); - co_await async_relay_store_body<2_KiB>(server_socket, server_socket, - b, p, sr, w); + auto body_size = get_content_length(p.get()); + if (!body_size.has_value()) { + throw std::runtime_error("no content length"); + } + co_await async_relay_store_body<1_KiB>(server_socket, server_socket, + b, w, *body_size); }, boost::asio::use_future) .get(); @@ -257,15 +261,16 @@ BOOST_AUTO_TEST_CASE(test_relay_body) { co_await async_read_header(server_socket, b, p); transform(p.get()); co_await async_write_header(server_socket, sr); + auto body_size = get_content_length(p.get()); + if (!body_size.has_value()) { + throw std::runtime_error("no content length"); + } + co_await async_relay_body<1_KiB>(server_socket, server_socket, b, + *body_size); }, boost::asio::use_future) .get(); - co_spawn(ioc, - async_relay_body<2_KiB>(server_socket, server_socket, b, p, sr), - boost::asio::use_future) - .get(); - work_guard.reset(); thread.join(); From 2df5e946c49e7cb33f9afd783d30c16e79cfda28 Mon Sep 17 00:00:00 2001 From: Sungsik Date: Mon, 22 Sep 2025 14:59:24 +0200 Subject: [PATCH 25/35] Parse/serialize header on cache --- src/proxy/handler.cpp | 40 ++++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/src/proxy/handler.cpp b/src/proxy/handler.cpp index 725043d1f..41f93321b 100644 --- a/src/proxy/handler.cpp +++ b/src/proxy/handler.cpp @@ -68,9 +68,28 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { co_await b.consume(); LOG_INFO() << peer << ": done reading complete request"; - auto header = std::vector(reader->get_header_size()); + auto header_size = + std::vector(reader->get_header_size()); + auto header = co_await reader->get(header_size); + + boost::beast::http::response_parser< + boost::beast::http::empty_body> + parser; + boost::beast::http::response_serializer< + boost::beast::http::empty_body, + boost::beast::http::fields> + serializer{parser.get()}; + + parser.body_limit( + std::numeric_limits::max()); + boost::system::error_code ec; + parser.put(boost::asio::const_buffer(header), ec); + if (ec) { + throw boost::system::system_error(ec); + } + + co_await async_write_header(s, serializer); - co_await async_write(s, co_await reader->get(header)); co_await cache::async_write<16_MiB>(incoming, *reader); LOG_INFO() << peer << ": cache result served"; @@ -85,12 +104,11 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { expect && *expect == "100-continue") { LOG_INFO() << req->peer() << ": forwarding 100 CONTINUE"; // TODO timeout - boost::beast::http::parser + boost::beast::http::response_parser< + boost::beast::http::empty_body> p; - boost::beast::http::serializer + boost::beast::http::response_serializer< + boost::beast::http::empty_body, boost::beast::http::fields> sr{p.get()}; co_await boost::beast::http::async_read_header(outgoing, o_buffer, p); @@ -109,13 +127,11 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { // forwarding response // TODO: alias default parser and serializer for relaying - boost::beast::http::parser + boost::beast::http::response_parser p; p.body_limit(std::numeric_limits::max()); - boost::beast::http::serializer< - false, boost::beast::http::double_buffer_body, - boost::beast::http::fields> + boost::beast::http::response_serializer< + boost::beast::http::empty_body, boost::beast::http::fields> sr{p.get()}; LOG_INFO() << peer << ": reading header from downstream"; From 05269870f603e2e9e329964689fa4f2b5f7943a1 Mon Sep 17 00:00:00 2001 From: Sungsik Date: Mon, 22 Sep 2025 20:43:46 +0200 Subject: [PATCH 26/35] Clean up namespace complication & remove TODOs --- src/common/etcd/impl/hostports_observer.h | 1 - .../telemetry/trace}/awaitable_operators.h | 3 +- src/common/utils/pointer_traits.h | 24 +- .../commands/iam/delete_access_key.cpp | 1 - src/proxy/asio.h | 14 + src/proxy/cache/asio.h | 375 ------------------ src/proxy/cache/disk/body.h | 4 +- src/proxy/cache/disk/deletion_queue.h | 1 - src/proxy/cache/disk/http.h | 93 +++++ src/proxy/cache/disk/manager.h | 1 - src/proxy/handler.cpp | 96 ++--- src/proxy/http.h | 158 ++++++++ test/unit/test_disk_cache_body.cpp | 91 +---- test/unit/test_storage_group_externals.cpp | 1 - 14 files changed, 325 insertions(+), 538 deletions(-) rename src/{proxy/cache => common/telemetry/trace}/awaitable_operators.h (99%) create mode 100644 src/proxy/asio.h delete mode 100644 src/proxy/cache/asio.h create mode 100644 src/proxy/cache/disk/http.h create mode 100644 src/proxy/http.h diff --git a/src/common/etcd/impl/hostports_observer.h b/src/common/etcd/impl/hostports_observer.h index ec6829368..3bcde61c7 100644 --- a/src/common/etcd/impl/hostports_observer.h +++ b/src/common/etcd/impl/hostports_observer.h @@ -126,7 +126,6 @@ class hostports_observer : public subscriber_observer { service_factory m_service_factory; std::vector>> m_observers; - // TODO: Modify this to vector and get num_storages on the constructor std::map> m_clients; std::atomic m_client_count{0}; }; diff --git a/src/proxy/cache/awaitable_operators.h b/src/common/telemetry/trace/awaitable_operators.h similarity index 99% rename from src/proxy/cache/awaitable_operators.h rename to src/common/telemetry/trace/awaitable_operators.h index fbc7e91f6..c6a0ff619 100644 --- a/src/proxy/cache/awaitable_operators.h +++ b/src/common/telemetry/trace/awaitable_operators.h @@ -1,8 +1,9 @@ #pragma once -#include #include +#include + namespace boost { namespace asio { namespace experimental { diff --git a/src/common/utils/pointer_traits.h b/src/common/utils/pointer_traits.h index f1b1dcdcb..a0504c113 100644 --- a/src/common/utils/pointer_traits.h +++ b/src/common/utils/pointer_traits.h @@ -8,28 +8,11 @@ namespace uh::cluster { struct pointer_traits { - /** - * TODO: Let's remove this: The storage layer will receive the storage - * address space pointer, so they do not need to call this function - * themselves - * - * The data store internal pointer is the low number of uint128_t - * - * @param global_pointer - * @return internal data store pointer - */ constexpr static const inline std::size_t group_id_bit_offset = 32 + 64; struct rr { constexpr static const inline std::size_t storage_id_bit_offset = 64; - inline static std::pair - get_storage_pointer(pointer global_pointer) { - std::size_t storage_id = (global_pointer >> 64) & 0xFFFFFFFF; - std::size_t storage_ptr = static_cast(global_pointer); - return {storage_id, storage_ptr}; - } - /** * @param pointer * @param storage_id @@ -42,6 +25,13 @@ struct pointer_traits { (static_cast(storage_id) << storage_id_bit_offset) | storage_pointer; } + + inline static std::pair + get_storage_pointer(pointer global_pointer) { + std::size_t storage_id = (global_pointer >> 64) & 0xFFFFFFFF; + std::size_t storage_ptr = static_cast(global_pointer); + return {storage_id, storage_ptr}; + } }; struct ec { diff --git a/src/entrypoint/commands/iam/delete_access_key.cpp b/src/entrypoint/commands/iam/delete_access_key.cpp index 2f92a1242..ed453f9c0 100644 --- a/src/entrypoint/commands/iam/delete_access_key.cpp +++ b/src/entrypoint/commands/iam/delete_access_key.cpp @@ -17,7 +17,6 @@ coro delete_access_key::handle(ep::http::request& req) { if (username) { auto user = co_await m_users.find_by_key(*access_key); if (user.name != *username) { - // TODO: how? throw command_exception( ep::http::status::conflict, "UserNameMismatch", "AWS IAM implements sophisticated organizations/roles " diff --git a/src/proxy/asio.h b/src/proxy/asio.h new file mode 100644 index 000000000..0c5a1e963 --- /dev/null +++ b/src/proxy/asio.h @@ -0,0 +1,14 @@ +#pragma once + +#include +#include + +namespace boost::asio { +inline std::span get_span(boost::asio::const_buffer buffer) { + return {static_cast(buffer.data()), buffer.size()}; +} + +inline std::span get_span(boost::asio::mutable_buffer buffer) { + return {static_cast(buffer.data()), buffer.size()}; +} +} // namespace boost::asio diff --git a/src/proxy/cache/asio.h b/src/proxy/cache/asio.h deleted file mode 100644 index e59c6d20f..000000000 --- a/src/proxy/cache/asio.h +++ /dev/null @@ -1,375 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -using namespace boost::asio::experimental::awaitable_operators; - -namespace uh::cluster::proxy::cache { - -template -concept ReaderBodyType = requires(T r, std::span sv) { - { r.put(sv) } -> std::same_as>; -}; - -template -concept WriterBodyType = requires(T w, std::span sv) { - { w.get(sv) } -> std::same_as>>; -}; - -template -concept BodyType = requires { - typename T::writer; - typename T::reader; - requires WriterBodyType; - requires ReaderBodyType; -}; - -template typename Body::reader make_reader(Body& b) { - return typename Body::reader(b); -} - -template typename Body::writer make_writer(Body& b) { - return typename Body::writer(b); -} - -/* - * async_read gets stream, body and size for it's input. - * - * size can be replaced with parser implementation - */ -template -requires std::is_base_of_v -coro async_read(S& s, T& t, std::size_t size) { - auto&& reader = [&]() -> auto&& { - if constexpr (BodyType) { - return make_reader(t); - } else if constexpr (ReaderBodyType) { - return t; - } else { - static_assert(BodyType || ReaderBodyType, - "T must satisfy BodyType or ReaderBodyType"); - } - }(); - - while (size > 0) { - auto sv = co_await s.read(size); - if (sv.empty()) - break; - co_await reader.put(sv); - co_await s.consume(); - size -= sv.size(); - } -} - -inline std::span get_span(boost::asio::const_buffer buffer) { - return {static_cast(buffer.data()), buffer.size()}; -} - -inline std::span get_span(boost::asio::mutable_buffer buffer) { - return {static_cast(buffer.data()), buffer.size()}; -} - -/* - * It consumes automatically - * - * TODO: use socket instead of stream - * TODO: Choose which namespace we will use - */ -template -coro async_write(ep::http::stream& s, T& t) { - auto&& writer = [&]() -> auto&& { - if constexpr (BodyType) { - return make_writer(t); - } else if constexpr (WriterBodyType) { - return t; - } else { - static_assert(BodyType || WriterBodyType, - "T must satisfy BodyType or WriterBodyType"); - } - }(); - - char _buf[2][buffer_size]; - char* rbuf = _buf[0]; - char* wbuf = _buf[1]; - - for (auto data = co_await writer.get({rbuf, buffer_size}); !data.empty();) { - std::swap(rbuf, wbuf); - auto [d, _] = - co_await (writer.get({rbuf, buffer_size}) && s.write(data)); - data = d; - } -} - -template coro ignore_need_buffer(Awaitable&& op) { - boost::system::error_code ec; - co_await op(boost::asio::redirect_error(boost::asio::use_awaitable, ec)); - if (ec && ec != boost::beast::http::error::need_buffer) { - throw boost::system::system_error(ec); - } -} - -template std::string serialize_header(Message& msg) { - using body_type = typename std::decay_t::body_type; - using fields_type = typename std::decay_t::fields_type; - constexpr bool is_request = std::decay_t::is_request::value; - boost::beast::http::serializer sr{msg}; - sr.split(true); - std::string header_str; - while (!sr.is_done()) { - auto const buf = sr.get(); - // buffers_to_string handles any buffer sequence or single buffer - header_str += boost::beast::buffers_to_string(buf); - sr.consume(boost::asio::buffer_size(buf)); - } - return header_str; -} - -template -coro async_write_store_header(ServerSocketType& server_socket, - Serializer& sr, SyncType& sync) { - std::ostringstream oss; - boost::system::error_code ec; - sr.split(true); - write_ostream(oss, sr, ec); - auto header_str = oss.str(); - if (header_str.size() == 0) { - throw std::runtime_error("Could not serialize header"); - } - co_await (sync.put(header_str) && [&]() -> coro { - co_await async_write(server_socket, boost::asio::buffer(header_str)); - }()); - sync.set_header_size(header_str.size()); - co_return header_str.size(); -} - -template -coro async_relay_body(Incomming& in, Outgoing& out, - boost::beast::flat_buffer& b, - std::size_t payload_size) { - - if (payload_size > chunk_size) { - boost::beast::flat_buffer b2; - auto* rbuf = &b; - auto* wbuf = &b2; - - for (auto n = co_await async_read( - in, rbuf->prepare(std::min(payload_size, chunk_size) - - rbuf->data().size())); - n != 0;) { - std::swap(rbuf, wbuf); - wbuf->commit(n + wbuf->data().size()); - payload_size -= wbuf->data().size(); - auto new_n = co_await ( // - [&]() -> coro { - co_return co_await async_read( - in, rbuf->prepare(std::min(payload_size, chunk_size))); - }() && [&]() -> coro { - co_await async_write(out, get_span(wbuf->data())); - }()); - wbuf->consume(wbuf->data().size()); - n = new_n; - } - } else { - auto n = - co_await async_read(in, b.prepare(payload_size - b.data().size())); - b.commit(n + b.data().size()); - co_await async_write(out, get_span(b.data())); - b.consume(b.data().size()); - } - - b.shrink_to_fit(); -} - -template -std::optional get_content_length(const Message& msg) { - auto it = msg.find(boost::beast::http::field::content_length); - if (it != msg.end()) { - try { - return std::stoull( - std::string(it->value().data(), it->value().size())); - } catch (...) { - return std::nullopt; - } - } - return std::nullopt; -} - -template -coro async_relay_store_body(Incomming& in, Outgoing& out, - boost::beast::flat_buffer& b, - PayloadWriter& writer, - std::size_t payload_size) { - - if (payload_size > chunk_size) { - boost::beast::flat_buffer b2; - auto* rbuf = &b; - auto* wbuf = &b2; - - for (auto n = co_await async_read( - in, rbuf->prepare(std::min(payload_size, chunk_size) - - rbuf->data().size())); - n != 0;) { - std::swap(rbuf, wbuf); - wbuf->commit(n + wbuf->data().size()); - payload_size -= wbuf->data().size(); - auto new_n = co_await ( // - [&]() -> coro { - co_return co_await async_read( - in, rbuf->prepare(std::min(payload_size, chunk_size))); - }() && ([&]() -> coro { - co_await async_write(out, get_span(wbuf->data())); - }() && writer.put(get_span(wbuf->data())) // - ) // - ); - wbuf->consume(wbuf->data().size()); - n = new_n; - } - } else { - auto n = - co_await async_read(in, b.prepare(payload_size - b.data().size())); - b.commit(n + b.data().size()); - co_await ([&]() -> coro { - co_await async_write(out, get_span(b.data())); - }() && writer.put(get_span(b.data()))); - b.consume(b.data().size()); - } - - b.shrink_to_fit(); -} - -template -coro async_relay_store_body(AsyncReadStream& input, - AsyncWriteStream& output, - DynamicBuffer& buffer, Parser& p, - Serializer& sr, PayloadSync& sync) { - static_assert(boost::beast::is_async_write_stream::value, - "AsyncWriteStream requirements not met"); - static_assert(boost::beast::is_async_read_stream::value, - "AsyncReadStream requirements not met"); - - char _buf[2][buffer_size]; - char* rbuf = _buf[0]; - char* wbuf = _buf[1]; - - auto read = [&](std::span sv) -> coro { - p.get().body().rdata = sv.data(); - p.get().body().rsize = sv.size(); - co_await ignore_need_buffer( - [&](auto token) { return async_read(input, buffer, p, token); }); - - co_return sv.size() - p.get().body().rsize; - }; - - auto write = [&](std::span sv) -> coro { - p.get().body().more = sv.size() != 0; - p.get().body().wdata = sv.data(); - p.get().body().wsize = sv.size(); - co_await ignore_need_buffer( - [&](auto token) { return async_write(output, sr, token); }); - }; - - std::size_t total_bytes = 0; - for (auto bytes_read = co_await read({rbuf, buffer_size}); - !p.is_done() || !sr.is_done();) { - std::swap(rbuf, wbuf); - bytes_read = co_await ( - (read({rbuf, buffer_size}) && write({wbuf, bytes_read})) && - sync.put({wbuf, bytes_read})); - total_bytes += bytes_read; - } - co_return total_bytes; -} - -} // namespace uh::cluster::proxy::cache -namespace boost::beast::http { -// The detail namespace means "not public" -namespace detail { - -// This helper is needed for C++11. -// When invoked with a buffer sequence, writes the buffers `to the -// std::ostream`. -template class write_ostream_helper { - Serializer& sr_; - std::ostream& os_; - -public: - write_ostream_helper(Serializer& sr, std::ostream& os) - : sr_(sr), - os_(os) {} - - // This function is called by the serializer - template - void operator()(error_code& ec, ConstBufferSequence const& buffers) const { - // Error codes must be cleared on success - ec = {}; - - // Keep a running total of how much we wrote - std::size_t bytes_transferred = 0; - - // Loop over the buffer sequence - for (auto it = boost::asio::buffer_sequence_begin(buffers); - it != boost::asio::buffer_sequence_end(buffers); ++it) { - // This is the next buffer in the sequence - boost::asio::const_buffer const buffer = *it; - - // Write it to the std::ostream - os_.write(reinterpret_cast(buffer.data()), - buffer.size()); - - // If the std::ostream fails, convert it to an error code - if (os_.fail()) { - ec = make_error_code(errc::io_error); - return; - } - - // Adjust our running total - bytes_transferred += buffer_size(buffer); - } - - // Inform the serializer of the amount we consumed - sr_.consume(bytes_transferred); - } -}; - -} // namespace detail - -/** Write a message to a `std::ostream`. - - This function writes the serialized representation of the - HTTP/1 message to the sream. - - @param os The `std::ostream` to write to. - - @param msg The message to serialize. - - @param ec Set to the error, if any occurred. -*/ -template -void write_ostream(std::ostream& os, Serializer& sr, error_code& ec) { - - // This lambda is used as the "visit" function - detail::write_ostream_helper lambda{sr, os}; - do { - // In C++14 we could use a generic lambda but since we want - // to require only C++11, the lambda is written out by hand. - // This function call retrieves the next serialized buffers. - sr.next(ec, lambda); - if (ec) - return; - } while (!sr.is_done()); -} - -} // namespace boost::beast::http diff --git a/src/proxy/cache/disk/body.h b/src/proxy/cache/disk/body.h index 717af7533..0050223ae 100644 --- a/src/proxy/cache/disk/body.h +++ b/src/proxy/cache/disk/body.h @@ -1,10 +1,8 @@ /* - * HTTP writer/reader bodies which supports get/put API only + * Body writer/reader bodies which supports get/put API only */ #pragma once -#include - #include #include diff --git a/src/proxy/cache/disk/deletion_queue.h b/src/proxy/cache/disk/deletion_queue.h index d42b7cfc5..facd79e06 100644 --- a/src/proxy/cache/disk/deletion_queue.h +++ b/src/proxy/cache/disk/deletion_queue.h @@ -13,7 +13,6 @@ namespace uh::cluster::proxy::cache::disk { template class deletion_queue { public: void push(std::shared_ptr e) { - // TODO: Implement this method std::unique_lock lock(m_mutex); m_queue.push(e); m_current_size += e->data_size(); diff --git a/src/proxy/cache/disk/http.h b/src/proxy/cache/disk/http.h new file mode 100644 index 000000000..9684daa4c --- /dev/null +++ b/src/proxy/cache/disk/http.h @@ -0,0 +1,93 @@ +#pragma once + +#include + +#include + +#include + +namespace uh::cluster::proxy::cache::disk { + +template +coro async_write(SocketType& s, BodyType& reader) { + using boost::asio::experimental::awaitable_operators::operator&&; + + char _buf[2][buffer_size]; + char* rbuf = _buf[0]; + char* wbuf = _buf[1]; + + for (auto data = co_await reader.get({rbuf, buffer_size}); !data.empty();) { + std::swap(rbuf, wbuf); + auto d = + co_await (reader.get({rbuf, buffer_size}) && [&]() -> coro { + co_await async_write(s, boost::asio::const_buffer(data)); + }()); + data = d; + } +} + +template +coro async_write_store_header(ServerSocketType& server_socket, + Serializer& sr, SyncType& sync) { + using boost::asio::experimental::awaitable_operators::operator&&; + std::ostringstream oss; + boost::system::error_code ec; + sr.split(true); + write_ostream(oss, sr, ec); + auto header_str = oss.str(); + if (header_str.size() == 0) { + throw std::runtime_error("Could not serialize header"); + } + co_await (sync.put(header_str) && [&]() -> coro { + co_await async_write(server_socket, boost::asio::buffer(header_str)); + }()); + sync.set_header_size(header_str.size()); + co_return header_str.size(); +} + +template +coro async_relay_store_body(Incomming& in, Outgoing& out, + boost::beast::flat_buffer& b, + PayloadWriter& writer, + std::size_t payload_size) { + using boost::asio::experimental::awaitable_operators::operator&&; + + if (payload_size > chunk_size) { + boost::beast::flat_buffer b2; + auto* rbuf = &b; + auto* wbuf = &b2; + + for (auto n = co_await async_read( + in, rbuf->prepare(std::min(payload_size, chunk_size) - + rbuf->data().size())); + n != 0;) { + std::swap(rbuf, wbuf); + wbuf->commit(n + wbuf->data().size()); + payload_size -= wbuf->data().size(); + auto new_n = co_await ( // + [&]() -> coro { + co_return co_await async_read( + in, rbuf->prepare(std::min(payload_size, chunk_size))); + }() && ([&]() -> coro { + co_await async_write(out, get_span(wbuf->data())); + }() && writer.put(get_span(wbuf->data())) // + ) // + ); + wbuf->consume(wbuf->data().size()); + n = new_n; + } + } else { + auto n = + co_await async_read(in, b.prepare(payload_size - b.data().size())); + b.commit(n + b.data().size()); + co_await ([&]() -> coro { + co_await async_write(out, get_span(b.data())); + }() && writer.put(get_span(b.data()))); + b.consume(b.data().size()); + } + + b.shrink_to_fit(); +} + +} // namespace uh::cluster::proxy::cache::disk diff --git a/src/proxy/cache/disk/manager.h b/src/proxy/cache/disk/manager.h index 9365f2df2..8ba76b399 100644 --- a/src/proxy/cache/disk/manager.h +++ b/src/proxy/cache/disk/manager.h @@ -88,7 +88,6 @@ class manager { std::atomic m_current_size{0}; deletion_queue_t m_deletion_queue; - // TODO: spawn a background task to remove scoped_task m_task; manager(boost::asio::io_context& ioc, data_view& storage, diff --git a/src/proxy/handler.cpp b/src/proxy/handler.cpp index 41f93321b..03a834b0c 100644 --- a/src/proxy/handler.cpp +++ b/src/proxy/handler.cpp @@ -2,14 +2,21 @@ #include "forward_stream.h" +#include +#include + +#include +#include + #include #include #include #include #include -#include -using namespace uh::cluster::ep::http; +using namespace boost::beast; +using namespace boost::beast::http; + namespace uh::cluster::proxy { handler::handler( @@ -28,7 +35,7 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { forward_stream incoming(s, *ds); auto& outgoing{*ds}; - boost::beast::flat_buffer o_buffer; + flat_buffer o_buffer; for (;;) { /* @@ -36,18 +43,18 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { */ std::string id = generate_unique_id(); - raw_request rawreq; - std::optional resp; + ep::http::raw_request rawreq; + std::optional resp; try { - rawreq = co_await raw_request::read(incoming, peer); + rawreq = co_await ep::http::raw_request::read(incoming, peer); auto& r = rawreq.headers; LOG_INFO() << peer << ": incoming request: " << r.method_string() << " " << r.target(); incoming.set_mode(forward_stream::forwarding); - std::unique_ptr req = + std::unique_ptr req = co_await m_factory->create(incoming, rawreq); if (get_object::can_handle(*req)) { @@ -57,7 +64,6 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { LOG_INFO() << peer << ": handling from cache"; incoming.set_mode(forward_stream::deleting); - // TODO: forwarding request auto& b = req->body(); auto bs = b.buffer_size(); @@ -68,29 +74,19 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { co_await b.consume(); LOG_INFO() << peer << ": done reading complete request"; - auto header_size = - std::vector(reader->get_header_size()); - auto header = co_await reader->get(header_size); - - boost::beast::http::response_parser< - boost::beast::http::empty_body> - parser; - boost::beast::http::response_serializer< - boost::beast::http::empty_body, - boost::beast::http::fields> - serializer{parser.get()}; - - parser.body_limit( - std::numeric_limits::max()); - boost::system::error_code ec; - parser.put(boost::asio::const_buffer(header), ec); - if (ec) { - throw boost::system::system_error(ec); - } + + response_parser parser; + response_serializer serializer{ + parser.get()}; + + co_await async_read_header(reader, parser); + + const char* via_value = PROJECT_NAME " " PROJECT_VERSION; + parser.get().set(field::via, via_value); co_await async_write_header(s, serializer); - co_await cache::async_write<16_MiB>(incoming, *reader); + co_await async_write<16_MiB>(s, *reader); LOG_INFO() << peer << ": cache result served"; continue; @@ -104,14 +100,9 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { expect && *expect == "100-continue") { LOG_INFO() << req->peer() << ": forwarding 100 CONTINUE"; // TODO timeout - boost::beast::http::response_parser< - boost::beast::http::empty_body> - p; - boost::beast::http::response_serializer< - boost::beast::http::empty_body, boost::beast::http::fields> - sr{p.get()}; - co_await boost::beast::http::async_read_header(outgoing, - o_buffer, p); + response_parser p; + response_serializer sr{p.get()}; + co_await async_read_header(outgoing, o_buffer, p); co_await async_write_header(s, sr); } @@ -126,44 +117,39 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { co_await b.consume(); // forwarding response - // TODO: alias default parser and serializer for relaying - boost::beast::http::response_parser - p; + response_parser p; p.body_limit(std::numeric_limits::max()); - boost::beast::http::response_serializer< - boost::beast::http::empty_body, boost::beast::http::fields> - sr{p.get()}; + response_serializer sr{p.get()}; LOG_INFO() << peer << ": reading header from downstream"; - co_await boost::beast::http::async_read_header(outgoing, o_buffer, - p); + co_await async_read_header(outgoing, o_buffer, p); // co_await async_write_header(s, sr); - if (r.method() == boost::beast::http::verb::head) { + if (r.method() == verb::head) { LOG_INFO() << peer << ": HEAD request, skipping body relay"; sr.split(true); - co_await boost::beast::http::async_write_header(s, sr); + co_await async_write_header(s, sr); } else if (get_object::can_handle(*req)) { - cache::disk::writer writer(m_dv); - co_await cache::async_write_store_header(s, sr, writer); - auto body_size = cache::get_content_length(p.get()); + auto writer = cache::disk::writer{m_dv}; + co_await async_write_store_header(s, sr, writer); + auto body_size = get_content_length(p.get()); if (!body_size.has_value()) { throw std::runtime_error("no content length"); } - co_await cache::async_relay_store_body<32_MiB>( - outgoing, s, o_buffer, writer, *body_size); + co_await async_relay_store_body<32_MiB>(outgoing, s, o_buffer, + writer, *body_size); co_await m_mgr.put( cache::disk::object_metadata{req->object_key()}, writer); } else { - co_await boost::beast::http::async_write_header(s, sr); - auto body_size = cache::get_content_length(p.get()); + co_await async_write_header(s, sr); + auto body_size = get_content_length(p.get()); if (!body_size.has_value()) { throw std::runtime_error("no content length"); } - co_await cache::async_relay_body<4_KiB>(outgoing, s, o_buffer, - *body_size); + co_await async_relay_buffer<4_KiB>(outgoing, s, o_buffer, + *body_size); } LOG_INFO() << peer << ": done"; diff --git a/src/proxy/http.h b/src/proxy/http.h new file mode 100644 index 000000000..6e6de83b9 --- /dev/null +++ b/src/proxy/http.h @@ -0,0 +1,158 @@ +#pragma once + +#include +#include + +#include +#include + +namespace boost::beast::http { + +// The detail namespace means "not public" +namespace detail { + +// This helper is needed for C++11. +// When invoked with a buffer sequence, writes the buffers `to the +// std::ostream`. +template class write_ostream_helper { + Serializer& sr_; + std::ostream& os_; + +public: + write_ostream_helper(Serializer& sr, std::ostream& os) + : sr_(sr), + os_(os) {} + + // This function is called by the serializer + template + void operator()(error_code& ec, ConstBufferSequence const& buffers) const { + // Error codes must be cleared on success + ec = {}; + + // Keep a running total of how much we wrote + std::size_t bytes_transferred = 0; + + // Loop over the buffer sequence + for (auto it = boost::asio::buffer_sequence_begin(buffers); + it != boost::asio::buffer_sequence_end(buffers); ++it) { + // This is the next buffer in the sequence + boost::asio::const_buffer const buffer = *it; + + // Write it to the std::ostream + os_.write(reinterpret_cast(buffer.data()), + buffer.size()); + + // If the std::ostream fails, convert it to an error code + if (os_.fail()) { + ec = make_error_code(errc::io_error); + return; + } + + // Adjust our running total + bytes_transferred += buffer_size(buffer); + } + + // Inform the serializer of the amount we consumed + sr_.consume(bytes_transferred); + } +}; + +} // namespace detail + +/** Write a message to a `std::ostream`. + + This function writes the serialized representation of the + HTTP/1 message to the sream. + + @param os The `std::ostream` to write to. + + @param msg The message to serialize. + + @param ec Set to the error, if any occurred. +*/ +template +void write_ostream(std::ostream& os, Serializer& sr, error_code& ec) { + + // This lambda is used as the "visit" function + detail::write_ostream_helper lambda{sr, os}; + do { + // In C++14 we could use a generic lambda but since we want + // to require only C++11, the lambda is written out by hand. + // This function call retrieves the next serialized buffers. + sr.next(ec, lambda); + if (ec) + return; + } while (!sr.is_done()); +} + +template +std::optional get_content_length(const Message& msg) { + auto it = msg.find(boost::beast::http::field::content_length); + if (it != msg.end()) { + try { + return std::stoull( + std::string(it->value().data(), it->value().size())); + } catch (...) { + return std::nullopt; + } + } + return std::nullopt; +} + +} // namespace boost::beast::http + +namespace uh::cluster::proxy { + +template +coro async_read_header(const Reader& reader, Parser& parser) { + auto header_size = std::vector(reader->get_header_size()); + auto header = co_await reader->get(header_size); + + parser.body_limit(std::numeric_limits::max()); + boost::system::error_code ec; + parser.put(boost::asio::const_buffer(header), ec); + if (ec) { + throw boost::system::system_error(ec); + } +} + +template +coro async_relay_buffer(Incomming& in, Outgoing& out, + boost::beast::flat_buffer& b, + std::size_t payload_size) { + using boost::asio::experimental::awaitable_operators::operator&&; + + if (payload_size > chunk_size) { + boost::beast::flat_buffer b2; + auto* rbuf = &b; + auto* wbuf = &b2; + + for (auto n = co_await async_read( + in, rbuf->prepare(std::min(payload_size, chunk_size) - + rbuf->data().size())); + n != 0;) { + std::swap(rbuf, wbuf); + wbuf->commit(n + wbuf->data().size()); + payload_size -= wbuf->data().size(); + auto new_n = co_await ( // + [&]() -> coro { + co_return co_await async_read( + in, rbuf->prepare(std::min(payload_size, chunk_size))); + }() && [&]() -> coro { + co_await async_write(out, get_span(wbuf->data())); + }()); + wbuf->consume(wbuf->data().size()); + n = new_n; + } + } else { + auto n = + co_await async_read(in, b.prepare(payload_size - b.data().size())); + b.commit(n + b.data().size()); + co_await async_write(out, get_span(b.data())); + b.consume(b.data().size()); + } + + b.shrink_to_fit(); +} + +} // namespace uh::cluster::proxy diff --git a/test/unit/test_disk_cache_body.cpp b/test/unit/test_disk_cache_body.cpp index 692481a43..965c5fe98 100644 --- a/test/unit/test_disk_cache_body.cpp +++ b/test/unit/test_disk_cache_body.cpp @@ -5,10 +5,10 @@ #include #include +#include +#include #include -#include -#include #include #include @@ -19,77 +19,6 @@ namespace uh::cluster::proxy::cache::disk { BOOST_FIXTURE_TEST_SUITE(a_disk_cache_body, dedupe_fixture) -BOOST_AUTO_TEST_CASE(supports_read) { - std::string data = random_string(64); - std::string header = "POST /upload HTTP/1.1\r\n" - "Host: localhost\r\n" - "Content-Length: " + - std::to_string(data.size()) + "\r\n\r\n"; - std::string req = header + data; - - std::cout << req << std::endl; - - // Set up TCP sockets - boost::asio::ip::tcp::acceptor acceptor(m_ioc, - {boost::asio::ip::tcp::v4(), 0}); - auto endpoint = acceptor.local_endpoint(); - - boost::asio::ip::tcp::socket server_sock(m_ioc); - boost::asio::ip::tcp::socket client_sock(m_ioc); - - client_sock.connect(endpoint); - acceptor.accept(server_sock); - - // Client writes HTTP request - auto written_size = - boost::asio::write(client_sock, boost::asio::buffer(req)); - - BOOST_TEST(written_size == req.size()); - - // Read header - ep::http::socket_stream stream(server_sock); - auto buffer = boost::asio::co_spawn(m_ioc, stream.read_until("\r\n\r\n"), - boost::asio::use_future) - .get(); - - BOOST_TEST(!buffer.empty()); - BOOST_TEST(buffer.size() == header.size()); - BOOST_TEST(std::string(buffer.data(), buffer.size()) == header); - - boost::beast::http::request_parser parser; - parser.body_limit((std::numeric_limits::max)()); - boost::beast::error_code ec; - // parser.put(boost::asio::buffer(buffer), ec); - parser.put(boost::asio::buffer(header.data(), header.size()), ec); - - auto res = parser.get(); - - BOOST_TEST(parser.is_header_done()); - - std::size_t content_length = std::stoul(res.at("Content-Length")); - - BOOST_TEST(content_length == data.size()); - - // 6. Read body using async_read and reader_body - writer w(data_view); - boost::asio::co_spawn(m_ioc, async_read(stream, w, content_length), - boost::asio::use_future) - .get(); - - // 7. Verify w was stored and can be read back - auto objh = w.get_object_handle(); - BOOST_TEST(objh.data_size() == data.size()); - - std::vector buf(data.size()); - boost::asio::co_spawn( - m_ioc, - data_view.read_address(objh.get_address(), - std::span{buf.data(), buf.size()}), - boost::asio::use_future) - .get(); - BOOST_TEST(std::string(buf.data(), buf.size()) == data); -} - BOOST_AUTO_TEST_CASE(supports_write) { std::string data = random_string(64); std::string header = "POST /download HTTP/1.1\r\n" @@ -119,15 +48,13 @@ BOOST_AUTO_TEST_CASE(supports_write) { client_sock.connect(endpoint); acceptor.accept(server_sock); - ep::http::socket_stream stream(client_sock); - // Client writes HTTP response header auto written_size = boost::asio::write(client_sock, boost::asio::buffer(header)); BOOST_TEST(written_size == header.size()); // Client writes r using async_write and writer_body - boost::asio::co_spawn(m_ioc, async_write<16_KiB>(stream, r), + boost::asio::co_spawn(m_ioc, async_write<16_KiB>(client_sock, r), boost::asio::use_future) .get(); @@ -168,8 +95,8 @@ BOOST_AUTO_TEST_CASE(goes_with_relay_store_body) { boost::beast::flat_buffer b; - parser p; - serializer sr{p.get()}; + parser p; + serializer sr{p.get()}; writer w(data_view); @@ -252,8 +179,8 @@ BOOST_AUTO_TEST_CASE(test_relay_body) { auto work_guard = boost::asio::make_work_guard(ioc.get_executor()); auto thread = std::thread([&ioc] { ioc.run(); }); - parser p; - serializer sr{p.get()}; + parser p; + serializer sr{p.get()}; co_spawn( ioc, @@ -265,8 +192,8 @@ BOOST_AUTO_TEST_CASE(test_relay_body) { if (!body_size.has_value()) { throw std::runtime_error("no content length"); } - co_await async_relay_body<1_KiB>(server_socket, server_socket, b, - *body_size); + co_await async_relay_buffer<1_KiB>(server_socket, server_socket, b, + *body_size); }, boost::asio::use_future) .get(); diff --git a/test/unit/test_storage_group_externals.cpp b/test/unit/test_storage_group_externals.cpp index c5bbde589..ee02a7dd4 100644 --- a/test/unit/test_storage_group_externals.cpp +++ b/test/unit/test_storage_group_externals.cpp @@ -42,7 +42,6 @@ BOOST_AUTO_TEST_CASE(is_watched_well) { std::promise p; std::future f = p.get_future(); - // TODO: Change lambda input type to void. auto subscriber = externals_subscriber( m_etcd, group_id, num_storages, service_factory(m_ioc, 2), [&]() { p.set_value(); }); From 23671f8bf1fc84164e77445370f171443670d3bc Mon Sep 17 00:00:00 2001 From: Sungsik Date: Tue, 23 Sep 2025 08:58:58 +0200 Subject: [PATCH 27/35] Handle when needed bytes are alread read when reading header --- src/proxy/http.h | 71 ++++++++++++++++++++++++++++++------------------ 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/src/proxy/http.h b/src/proxy/http.h index 6e6de83b9..f58c96ab7 100644 --- a/src/proxy/http.h +++ b/src/proxy/http.h @@ -3,8 +3,13 @@ #include #include +#include + +#include #include + #include +#include namespace boost::beast::http { @@ -122,34 +127,46 @@ coro async_relay_buffer(Incomming& in, Outgoing& out, std::size_t payload_size) { using boost::asio::experimental::awaitable_operators::operator&&; - if (payload_size > chunk_size) { - boost::beast::flat_buffer b2; - auto* rbuf = &b; - auto* wbuf = &b2; - - for (auto n = co_await async_read( - in, rbuf->prepare(std::min(payload_size, chunk_size) - - rbuf->data().size())); - n != 0;) { - std::swap(rbuf, wbuf); - wbuf->commit(n + wbuf->data().size()); - payload_size -= wbuf->data().size(); - auto new_n = co_await ( // - [&]() -> coro { - co_return co_await async_read( - in, rbuf->prepare(std::min(payload_size, chunk_size))); - }() && [&]() -> coro { - co_await async_write(out, get_span(wbuf->data())); - }()); - wbuf->consume(wbuf->data().size()); - n = new_n; - } + if (b.data().size() >= payload_size) { + auto sv = std::span( + static_cast(b.data().data()), payload_size); + co_await async_write(out, sv); + b.consume(sv.size()); + } else { - auto n = - co_await async_read(in, b.prepare(payload_size - b.data().size())); - b.commit(n + b.data().size()); - co_await async_write(out, get_span(b.data())); - b.consume(b.data().size()); + if (payload_size > chunk_size) { + boost::beast::flat_buffer b2; + auto* rbuf = &b; + auto* wbuf = &b2; + + for (auto n = co_await async_read( + in, rbuf->prepare(std::min(payload_size, chunk_size) - + rbuf->data().size())); + n != 0;) { + std::swap(rbuf, wbuf); + wbuf->commit(n); + if (wbuf->data().size() != std::min(payload_size, chunk_size)) { + throw std::runtime_error("buffer size mismatch"); + } + payload_size -= wbuf->data().size(); + auto new_n = co_await ( // + [&]() -> coro { + co_return co_await async_read( + in, + rbuf->prepare(std::min(payload_size, chunk_size))); + }() && [&]() -> coro { + co_await async_write(out, get_span(wbuf->data())); + }()); + wbuf->consume(wbuf->data().size()); + n = new_n; + } + } else { + auto n = co_await async_read( + in, b.prepare(payload_size - b.data().size())); + b.commit(n); + co_await async_write(out, get_span(b.data())); + b.consume(b.data().size()); + } } b.shrink_to_fit(); From 559f00732685c0dbec292afc003e31f36c353745 Mon Sep 17 00:00:00 2001 From: Sungsik Date: Tue, 23 Sep 2025 09:07:08 +0200 Subject: [PATCH 28/35] Set maximum size for flat_buffer --- src/proxy/handler.cpp | 21 ++++++++++++++------- src/proxy/http.h | 2 +- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/proxy/handler.cpp b/src/proxy/handler.cpp index 03a834b0c..cd043eeeb 100644 --- a/src/proxy/handler.cpp +++ b/src/proxy/handler.cpp @@ -35,9 +35,16 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { forward_stream incoming(s, *ds); auto& outgoing{*ds}; - flat_buffer o_buffer; - for (;;) { + constexpr std::size_t buffer_size_to_load = 16_MiB; + + constexpr std::size_t buffer_size_to_relay_and_store = 32_MiB; + constexpr std::size_t buffer_size_to_relay = 4_KiB; + + flat_buffer o_buffer( + std::max(buffer_size_to_relay, buffer_size_to_relay_and_store)); + + for (;;) { /* * Note: lifetime of response must not exceed lifetime of request. */ @@ -86,7 +93,7 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { co_await async_write_header(s, serializer); - co_await async_write<16_MiB>(s, *reader); + co_await async_write(s, *reader); LOG_INFO() << peer << ": cache result served"; continue; @@ -137,8 +144,8 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { if (!body_size.has_value()) { throw std::runtime_error("no content length"); } - co_await async_relay_store_body<32_MiB>(outgoing, s, o_buffer, - writer, *body_size); + co_await async_relay_store_body( + outgoing, s, o_buffer, writer, *body_size); co_await m_mgr.put( cache::disk::object_metadata{req->object_key()}, writer); @@ -148,8 +155,8 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { if (!body_size.has_value()) { throw std::runtime_error("no content length"); } - co_await async_relay_buffer<4_KiB>(outgoing, s, o_buffer, - *body_size); + co_await async_relay_buffer( + outgoing, s, o_buffer, *body_size); } LOG_INFO() << peer << ": done"; diff --git a/src/proxy/http.h b/src/proxy/http.h index f58c96ab7..bb7fe812c 100644 --- a/src/proxy/http.h +++ b/src/proxy/http.h @@ -135,7 +135,7 @@ coro async_relay_buffer(Incomming& in, Outgoing& out, } else { if (payload_size > chunk_size) { - boost::beast::flat_buffer b2; + boost::beast::flat_buffer b2(chunk_size); auto* rbuf = &b; auto* wbuf = &b2; From 9b0e344b41322e90823684c427ad4ad5c6af2520 Mon Sep 17 00:00:00 2001 From: Sungsik Date: Tue, 23 Sep 2025 12:38:12 +0200 Subject: [PATCH 29/35] Refactoring --- src/proxy/asio.h | 21 +++++ src/proxy/cache/disk/{body.h => disk_io.h} | 27 +++---- src/proxy/cache/disk/http.h | 93 ---------------------- src/proxy/cache/disk/manager.h | 8 +- src/proxy/handler.cpp | 30 +++---- src/proxy/http.h | 69 +++++++++++----- src/proxy/socket_io.h | 41 ++++++++++ src/proxy/tee_io.h | 28 +++++++ test/unit/test_disk_cache_body.cpp | 26 +++--- 9 files changed, 184 insertions(+), 159 deletions(-) rename src/proxy/cache/disk/{body.h => disk_io.h} (80%) delete mode 100644 src/proxy/cache/disk/http.h create mode 100644 src/proxy/socket_io.h create mode 100644 src/proxy/tee_io.h diff --git a/src/proxy/asio.h b/src/proxy/asio.h index 0c5a1e963..50a70f5a7 100644 --- a/src/proxy/asio.h +++ b/src/proxy/asio.h @@ -4,6 +4,7 @@ #include namespace boost::asio { + inline std::span get_span(boost::asio::const_buffer buffer) { return {static_cast(buffer.data()), buffer.size()}; } @@ -11,4 +12,24 @@ inline std::span get_span(boost::asio::const_buffer buffer) { inline std::span get_span(boost::asio::mutable_buffer buffer) { return {static_cast(buffer.data()), buffer.size()}; } + } // namespace boost::asio + +namespace std { + +template +inline std::span get_span(const std::vector& v) { + return {reinterpret_cast(v.data()), v.size() * sizeof(T)}; +} + +template inline std::span get_span(std::vector& v) { + return {reinterpret_cast(v.data()), v.size() * sizeof(T)}; +} + +inline std::span get_span(const std::string& s) { + return {s.data(), s.size()}; +} + +inline std::span get_span(std::string& s) { return {s.data(), s.size()}; } + +} // namespace std diff --git a/src/proxy/cache/disk/body.h b/src/proxy/cache/disk/disk_io.h similarity index 80% rename from src/proxy/cache/disk/body.h rename to src/proxy/cache/disk/disk_io.h index 0050223ae..53dfdadd1 100644 --- a/src/proxy/cache/disk/body.h +++ b/src/proxy/cache/disk/disk_io.h @@ -1,5 +1,5 @@ /* - * Body writer/reader bodies which supports get/put API only + * Sync/source for disk, which supports put/get API */ #pragma once @@ -15,16 +15,12 @@ namespace uh::cluster::proxy::cache::disk { -class writer { +class disk_sync { public: - writer(storage::data_view& writer) + disk_sync(storage::data_view& writer) : m_storage{writer}, m_addr{} {} - template coro put(const T& s) { - return put(std::span(s.data(), s.size())); - } - coro put(std::span sv) { if (sv.size() == 0) { co_return; @@ -53,23 +49,20 @@ class writer { std::size_t m_header_size{0}; }; -class reader { +class disk_source { public: - reader(storage::data_view& storage, std::shared_ptr objh) + disk_source(storage::data_view& storage, + std::shared_ptr objh) : m_storage(storage), m_objh{std::move(objh)} {} - reader(const reader&) = delete; - reader& operator=(const reader&) = delete; - reader(reader&&) = delete; - reader& operator=(reader&&) = delete; + disk_source(const disk_source&) = delete; + disk_source& operator=(const disk_source&) = delete; + disk_source(disk_source&&) = delete; + disk_source& operator=(disk_source&&) = delete; std::size_t get_header_size() const { return m_objh->header_size(); } - template coro> get(T& s) { - return get(std::span(s.data(), s.size())); - } - coro> get(std::span buffer) { std::size_t read_size = 0; address partial_addr; diff --git a/src/proxy/cache/disk/http.h b/src/proxy/cache/disk/http.h deleted file mode 100644 index 9684daa4c..000000000 --- a/src/proxy/cache/disk/http.h +++ /dev/null @@ -1,93 +0,0 @@ -#pragma once - -#include - -#include - -#include - -namespace uh::cluster::proxy::cache::disk { - -template -coro async_write(SocketType& s, BodyType& reader) { - using boost::asio::experimental::awaitable_operators::operator&&; - - char _buf[2][buffer_size]; - char* rbuf = _buf[0]; - char* wbuf = _buf[1]; - - for (auto data = co_await reader.get({rbuf, buffer_size}); !data.empty();) { - std::swap(rbuf, wbuf); - auto d = - co_await (reader.get({rbuf, buffer_size}) && [&]() -> coro { - co_await async_write(s, boost::asio::const_buffer(data)); - }()); - data = d; - } -} - -template -coro async_write_store_header(ServerSocketType& server_socket, - Serializer& sr, SyncType& sync) { - using boost::asio::experimental::awaitable_operators::operator&&; - std::ostringstream oss; - boost::system::error_code ec; - sr.split(true); - write_ostream(oss, sr, ec); - auto header_str = oss.str(); - if (header_str.size() == 0) { - throw std::runtime_error("Could not serialize header"); - } - co_await (sync.put(header_str) && [&]() -> coro { - co_await async_write(server_socket, boost::asio::buffer(header_str)); - }()); - sync.set_header_size(header_str.size()); - co_return header_str.size(); -} - -template -coro async_relay_store_body(Incomming& in, Outgoing& out, - boost::beast::flat_buffer& b, - PayloadWriter& writer, - std::size_t payload_size) { - using boost::asio::experimental::awaitable_operators::operator&&; - - if (payload_size > chunk_size) { - boost::beast::flat_buffer b2; - auto* rbuf = &b; - auto* wbuf = &b2; - - for (auto n = co_await async_read( - in, rbuf->prepare(std::min(payload_size, chunk_size) - - rbuf->data().size())); - n != 0;) { - std::swap(rbuf, wbuf); - wbuf->commit(n + wbuf->data().size()); - payload_size -= wbuf->data().size(); - auto new_n = co_await ( // - [&]() -> coro { - co_return co_await async_read( - in, rbuf->prepare(std::min(payload_size, chunk_size))); - }() && ([&]() -> coro { - co_await async_write(out, get_span(wbuf->data())); - }() && writer.put(get_span(wbuf->data())) // - ) // - ); - wbuf->consume(wbuf->data().size()); - n = new_n; - } - } else { - auto n = - co_await async_read(in, b.prepare(payload_size - b.data().size())); - b.commit(n + b.data().size()); - co_await ([&]() -> coro { - co_await async_write(out, get_span(b.data())); - }() && writer.put(get_span(b.data()))); - b.consume(b.data().size()); - } - - b.shrink_to_fit(); -} - -} // namespace uh::cluster::proxy::cache::disk diff --git a/src/proxy/cache/disk/manager.h b/src/proxy/cache/disk/manager.h index 8ba76b399..ad138b708 100644 --- a/src/proxy/cache/disk/manager.h +++ b/src/proxy/cache/disk/manager.h @@ -1,7 +1,7 @@ #pragma once -#include #include +#include #include #include @@ -32,7 +32,7 @@ class manager { * * It removed address information from the given body. */ - coro put(object_metadata key, writer& w) { + coro put(object_metadata key, disk_sync& w) { auto objh = w.get_object_handle(); auto obj_size = objh.data_size(); @@ -66,12 +66,12 @@ class manager { std::cout << "Total size after put: " << m_current_size << std::endl; } - std::unique_ptr get(object_metadata key) { + std::unique_ptr get(object_metadata key) { auto entry = m_cache->get(key); if (!entry) { return nullptr; } - return std::make_unique(m_storage, std::move(entry)); + return std::make_unique(m_storage, std::move(entry)); } static manager create(boost::asio::io_context& ioc, data_view& storage, diff --git a/src/proxy/handler.cpp b/src/proxy/handler.cpp index cd043eeeb..d825affbf 100644 --- a/src/proxy/handler.cpp +++ b/src/proxy/handler.cpp @@ -5,8 +5,9 @@ #include #include -#include -#include +#include +#include +#include #include #include @@ -65,9 +66,9 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { co_await m_factory->create(incoming, rawreq); if (get_object::can_handle(*req)) { - auto reader = + auto d_source = m_mgr.get(cache::disk::object_metadata{req->object_key()}); - if (reader) { + if (d_source) { LOG_INFO() << peer << ": handling from cache"; incoming.set_mode(forward_stream::deleting); @@ -86,14 +87,14 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { response_serializer serializer{ parser.get()}; - co_await async_read_header(reader, parser); + co_await async_read_header(d_source, parser); const char* via_value = PROJECT_NAME " " PROJECT_VERSION; parser.get().set(field::via, via_value); co_await async_write_header(s, serializer); - co_await async_write(s, *reader); + co_await async_write(s, *d_source); LOG_INFO() << peer << ": cache result served"; continue; @@ -130,24 +131,23 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { LOG_INFO() << peer << ": reading header from downstream"; co_await async_read_header(outgoing, o_buffer, p); - // co_await async_write_header(s, sr); if (r.method() == verb::head) { LOG_INFO() << peer << ": HEAD request, skipping body relay"; - sr.split(true); co_await async_write_header(s, sr); } else if (get_object::can_handle(*req)) { - auto writer = cache::disk::writer{m_dv}; - co_await async_write_store_header(s, sr, writer); + auto d_sync = cache::disk::disk_sync{m_dv}; + auto s_sync = socket_sync{s}; + co_await async_write_header(s, sr, d_sync); auto body_size = get_content_length(p.get()); if (!body_size.has_value()) { throw std::runtime_error("no content length"); } - co_await async_relay_store_body( - outgoing, s, o_buffer, writer, *body_size); + co_await async_read( + outgoing, o_buffer, *body_size, tee_sync(s_sync, d_sync)); co_await m_mgr.put( - cache::disk::object_metadata{req->object_key()}, writer); + cache::disk::object_metadata{req->object_key()}, d_sync); } else { co_await async_write_header(s, sr); @@ -155,8 +155,8 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { if (!body_size.has_value()) { throw std::runtime_error("no content length"); } - co_await async_relay_buffer( - outgoing, s, o_buffer, *body_size); + co_await async_read( + outgoing, o_buffer, *body_size, socket_sync(s)); } LOG_INFO() << peer << ": done"; diff --git a/src/proxy/http.h b/src/proxy/http.h index bb7fe812c..8c5beebfe 100644 --- a/src/proxy/http.h +++ b/src/proxy/http.h @@ -108,10 +108,10 @@ std::optional get_content_length(const Message& msg) { namespace uh::cluster::proxy { -template -coro async_read_header(const Reader& reader, Parser& parser) { - auto header_size = std::vector(reader->get_header_size()); - auto header = co_await reader->get(header_size); +template +coro async_read_header(const SourceType& source, Parser& parser) { + auto header_size = std::vector(source->get_header_size()); + auto header = co_await source->get(header_size); parser.body_limit(std::numeric_limits::max()); boost::system::error_code ec; @@ -121,16 +121,34 @@ coro async_read_header(const Reader& reader, Parser& parser) { } } -template -coro async_relay_buffer(Incomming& in, Outgoing& out, - boost::beast::flat_buffer& b, - std::size_t payload_size) { +template +coro async_write_header(ServerSocketType& server_socket, + Serializer& sr, SyncType& sync) { + using boost::asio::experimental::awaitable_operators::operator&&; + std::ostringstream oss; + boost::system::error_code ec; + sr.split(true); + write_ostream(oss, sr, ec); + auto header_str = oss.str(); + if (header_str.size() == 0) { + throw std::runtime_error("Could not serialize header"); + } + co_await (sync.put(header_str) && [&]() -> coro { + co_await async_write(server_socket, boost::asio::buffer(header_str)); + }()); + sync.set_header_size(header_str.size()); + co_return header_str.size(); +} + +template +coro async_read(Incomming& in, boost::beast::flat_buffer& b, + std::size_t payload_size, SyncType&& sync) { using boost::asio::experimental::awaitable_operators::operator&&; if (b.data().size() >= payload_size) { auto sv = std::span( static_cast(b.data().data()), payload_size); - co_await async_write(out, sv); + co_await sync.put(sv); b.consume(sv.size()); } else { @@ -149,14 +167,11 @@ coro async_relay_buffer(Incomming& in, Outgoing& out, throw std::runtime_error("buffer size mismatch"); } payload_size -= wbuf->data().size(); - auto new_n = co_await ( // - [&]() -> coro { - co_return co_await async_read( - in, - rbuf->prepare(std::min(payload_size, chunk_size))); - }() && [&]() -> coro { - co_await async_write(out, get_span(wbuf->data())); - }()); + auto new_n = co_await ([&]() -> coro { + co_return co_await async_read( + in, rbuf->prepare(std::min(payload_size, chunk_size))); + }() && sync.put(get_span(wbuf->data()))); + wbuf->consume(wbuf->data().size()); n = new_n; } @@ -164,7 +179,7 @@ coro async_relay_buffer(Incomming& in, Outgoing& out, auto n = co_await async_read( in, b.prepare(payload_size - b.data().size())); b.commit(n); - co_await async_write(out, get_span(b.data())); + co_await sync.put(get_span(b.data())); b.consume(b.data().size()); } } @@ -172,4 +187,22 @@ coro async_relay_buffer(Incomming& in, Outgoing& out, b.shrink_to_fit(); } +template +coro async_write(SocketType& s, SourceType& source) { + using boost::asio::experimental::awaitable_operators::operator&&; + + char _buf[2][buffer_size]; + char* rbuf = _buf[0]; + char* wbuf = _buf[1]; + + for (auto data = co_await source.get({rbuf, buffer_size}); !data.empty();) { + std::swap(rbuf, wbuf); + auto d = + co_await (source.get({rbuf, buffer_size}) && [&]() -> coro { + co_await async_write(s, boost::asio::const_buffer(data)); + }()); + data = d; + } +} + } // namespace uh::cluster::proxy diff --git a/src/proxy/socket_io.h b/src/proxy/socket_io.h new file mode 100644 index 000000000..4d8fdf31c --- /dev/null +++ b/src/proxy/socket_io.h @@ -0,0 +1,41 @@ +/* + * Sync/source, which supports get/put API only + */ +#pragma once + +#include + +namespace uh::cluster::proxy { + +template class socket_sync { +public: + socket_sync(SocketType& s) + : m_s{s} {} + + coro put(std::span sv) { + if (sv.size() == 0) { + co_return; + } + co_await boost::asio::async_write(m_s, boost::asio::buffer(sv)); + } + +private: + SocketType& m_s; +}; + +template class socket_source { +public: + socket_source(SocketType& s) + : m_s{s} {} + + coro> get(std::span buffer) { + auto n = + co_await boost::asio::async_read(m_s, boost::asio::buffer(buffer)); + co_return std::span(buffer.data(), n); + } + +private: + SocketType& m_s; +}; + +} // namespace uh::cluster::proxy diff --git a/src/proxy/tee_io.h b/src/proxy/tee_io.h new file mode 100644 index 000000000..088974e0d --- /dev/null +++ b/src/proxy/tee_io.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +namespace uh::cluster::proxy { + +template class tee_sync { +public: + tee_sync(T& t, U& u) + : m_t{t}, + m_u{u} {} + + coro put(std::span sv) { + using boost::asio::experimental::awaitable_operators::operator&&; + + if (sv.size() == 0) { + co_return; + } + co_await (m_t.put(sv) && m_u.put(sv)); + } + +private: + T& m_t; + U& m_u; +}; + +} // namespace uh::cluster::proxy diff --git a/test/unit/test_disk_cache_body.cpp b/test/unit/test_disk_cache_body.cpp index 965c5fe98..fccd63ebb 100644 --- a/test/unit/test_disk_cache_body.cpp +++ b/test/unit/test_disk_cache_body.cpp @@ -4,9 +4,10 @@ #include -#include -#include +#include #include +#include +#include #include @@ -37,7 +38,7 @@ BOOST_AUTO_TEST_CASE(supports_write) { auto objh = std::make_shared(std::move(addr)); BOOST_TEST(objh->data_size() == data.size()); - reader r(data_view, std::move(objh)); + disk_source source(data_view, std::move(objh)); // Set up TCP sockets boost::asio::ip::tcp::acceptor acceptor(m_ioc, @@ -53,8 +54,8 @@ BOOST_AUTO_TEST_CASE(supports_write) { boost::asio::write(client_sock, boost::asio::buffer(header)); BOOST_TEST(written_size == header.size()); - // Client writes r using async_write and writer_body - boost::asio::co_spawn(m_ioc, async_write<16_KiB>(client_sock, r), + // Client writes source using async_write and writer_body + boost::asio::co_spawn(m_ioc, async_write<16_KiB>(client_sock, source), boost::asio::use_future) .get(); @@ -98,20 +99,21 @@ BOOST_AUTO_TEST_CASE(goes_with_relay_store_body) { parser p; serializer sr{p.get()}; - writer w(data_view); + disk_sync dsync(data_view); + socket_sync ssync(server_socket); co_spawn( m_ioc, [&]() -> coro { auto n = co_await async_read_header(server_socket, b, p); - auto m = co_await async_write_store_header(server_socket, sr, w); + auto m = co_await async_write_header(server_socket, sr, dsync); BOOST_TEST(n == m); auto body_size = get_content_length(p.get()); if (!body_size.has_value()) { throw std::runtime_error("no content length"); } - co_await async_relay_store_body<1_KiB>(server_socket, server_socket, - b, w, *body_size); + co_await async_read<1_KiB>(server_socket, b, *body_size, + tee_sync(dsync, ssync)); }, boost::asio::use_future) .get(); @@ -132,7 +134,7 @@ BOOST_AUTO_TEST_CASE(goes_with_relay_store_body) { BOOST_TEST(output_str == std::string_view(raw_message.data(), raw_message.size())); - auto objh = w.get_object_handle(); + auto objh = dsync.get_object_handle(); BOOST_TEST(objh.data_size() == raw_message.size()); std::vector buf(raw_message.size()); @@ -192,8 +194,8 @@ BOOST_AUTO_TEST_CASE(test_relay_body) { if (!body_size.has_value()) { throw std::runtime_error("no content length"); } - co_await async_relay_buffer<1_KiB>(server_socket, server_socket, b, - *body_size); + co_await async_read<1_KiB>(server_socket, b, *body_size, + socket_sync(server_socket)); }, boost::asio::use_future) .get(); From 75327c17b222fbba10c2fe2f04e3c1abd4f0f07a Mon Sep 17 00:00:00 2001 From: Sungsik Date: Tue, 23 Sep 2025 13:08:42 +0200 Subject: [PATCH 30/35] Pipelined async_write_header --- src/proxy/handler.cpp | 19 ++++++++++++------- src/proxy/http.h | 37 +++++++++++++++++++++++-------------- 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/proxy/handler.cpp b/src/proxy/handler.cpp index d825affbf..95cee6f34 100644 --- a/src/proxy/handler.cpp +++ b/src/proxy/handler.cpp @@ -92,9 +92,10 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { const char* via_value = PROJECT_NAME " " PROJECT_VERSION; parser.get().set(field::via, via_value); - co_await async_write_header(s, serializer); - - co_await async_write(s, *d_source); + co_await async_write( + s, *d_source, [&]() -> coro { + co_await async_write_header(s, serializer); + }); LOG_INFO() << peer << ": cache result served"; continue; @@ -139,24 +140,28 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { } else if (get_object::can_handle(*req)) { auto d_sync = cache::disk::disk_sync{m_dv}; auto s_sync = socket_sync{s}; - co_await async_write_header(s, sr, d_sync); auto body_size = get_content_length(p.get()); if (!body_size.has_value()) { throw std::runtime_error("no content length"); } co_await async_read( - outgoing, o_buffer, *body_size, tee_sync(s_sync, d_sync)); + outgoing, o_buffer, *body_size, tee_sync(s_sync, d_sync), + [&]() -> coro { + co_await async_write_header(s, sr, d_sync); + }); co_await m_mgr.put( cache::disk::object_metadata{req->object_key()}, d_sync); } else { - co_await async_write_header(s, sr); auto body_size = get_content_length(p.get()); if (!body_size.has_value()) { throw std::runtime_error("no content length"); } co_await async_read( - outgoing, o_buffer, *body_size, socket_sync(s)); + outgoing, o_buffer, *body_size, socket_sync(s), + [&]() -> coro { + co_await async_write_header(s, sr); + }); } LOG_INFO() << peer << ": done"; diff --git a/src/proxy/http.h b/src/proxy/http.h index 8c5beebfe..c54b0f05e 100644 --- a/src/proxy/http.h +++ b/src/proxy/http.h @@ -141,25 +141,32 @@ coro async_write_header(ServerSocketType& server_socket, } template -coro async_read(Incomming& in, boost::beast::flat_buffer& b, - std::size_t payload_size, SyncType&& sync) { +coro async_read( + Incomming& in, boost::beast::flat_buffer& b, std::size_t payload_size, + SyncType&& sync, + std::function()> precursor = []() -> coro { co_return; }) { using boost::asio::experimental::awaitable_operators::operator&&; if (b.data().size() >= payload_size) { auto sv = std::span( static_cast(b.data().data()), payload_size); - co_await sync.put(sv); + co_await (sync.put(sv) && precursor()); b.consume(sv.size()); } else { + auto read = [&](auto& s, auto& buffer, + std::size_t required) -> coro { + co_return co_await async_read(s, buffer.prepare(required)); + }; if (payload_size > chunk_size) { boost::beast::flat_buffer b2(chunk_size); auto* rbuf = &b; auto* wbuf = &b2; - for (auto n = co_await async_read( - in, rbuf->prepare(std::min(payload_size, chunk_size) - - rbuf->data().size())); + for (auto n = co_await (read(in, *rbuf, + std::min(payload_size, chunk_size) - + rbuf->data().size()) && + precursor()); n != 0;) { std::swap(rbuf, wbuf); wbuf->commit(n); @@ -167,17 +174,16 @@ coro async_read(Incomming& in, boost::beast::flat_buffer& b, throw std::runtime_error("buffer size mismatch"); } payload_size -= wbuf->data().size(); - auto new_n = co_await ([&]() -> coro { - co_return co_await async_read( - in, rbuf->prepare(std::min(payload_size, chunk_size))); - }() && sync.put(get_span(wbuf->data()))); + auto new_n = co_await ( + read(in, *rbuf, std::min(payload_size, chunk_size)) && + sync.put(get_span(wbuf->data()))); wbuf->consume(wbuf->data().size()); n = new_n; } } else { - auto n = co_await async_read( - in, b.prepare(payload_size - b.data().size())); + auto n = co_await (read(in, b, payload_size - b.data().size()) && + precursor()); b.commit(n); co_await sync.put(get_span(b.data())); b.consume(b.data().size()); @@ -188,14 +194,17 @@ coro async_read(Incomming& in, boost::beast::flat_buffer& b, } template -coro async_write(SocketType& s, SourceType& source) { +coro async_write( + SocketType& s, SourceType& source, + std::function()> precursor = []() -> coro { co_return; }) { using boost::asio::experimental::awaitable_operators::operator&&; char _buf[2][buffer_size]; char* rbuf = _buf[0]; char* wbuf = _buf[1]; - for (auto data = co_await source.get({rbuf, buffer_size}); !data.empty();) { + for (auto data = co_await (source.get({rbuf, buffer_size}) && precursor()); + !data.empty();) { std::swap(rbuf, wbuf); auto d = co_await (source.get({rbuf, buffer_size}) && [&]() -> coro { From 7096c5c64381809b217ff8d2be3f9ab9ba49212d Mon Sep 17 00:00:00 2001 From: Sungsik Date: Tue, 23 Sep 2025 14:30:53 +0200 Subject: [PATCH 31/35] Apply review --- src/proxy/cache/disk/disk_io.h | 34 ++--- src/proxy/cache/disk/manager.h | 7 +- src/proxy/cache/double_buffer_body.h | 194 --------------------------- src/proxy/handler.cpp | 20 +-- src/proxy/http.h | 43 +++--- src/proxy/socket_io.h | 4 +- src/proxy/tee_io.h | 4 +- test/unit/test_disk_cache_body.cpp | 13 +- 8 files changed, 63 insertions(+), 256 deletions(-) delete mode 100644 src/proxy/cache/double_buffer_body.h diff --git a/src/proxy/cache/disk/disk_io.h b/src/proxy/cache/disk/disk_io.h index 53dfdadd1..e03eb9796 100644 --- a/src/proxy/cache/disk/disk_io.h +++ b/src/proxy/cache/disk/disk_io.h @@ -15,18 +15,22 @@ namespace uh::cluster::proxy::cache::disk { -class disk_sync { +class disk_sink { public: - disk_sync(storage::data_view& writer) + disk_sink(storage::data_view& writer) : m_storage{writer}, m_addr{} {} + disk_sink(const disk_sink&) = delete; + disk_sink& operator=(const disk_sink&) = delete; + disk_sink(disk_sink&&) = default; + disk_sink& operator=(disk_sink&&) = default; + coro put(std::span sv) { if (sv.size() == 0) { co_return; } - auto addr = co_await m_storage.write(sv, {0}); - m_hash.consume(sv); + auto addr = co_await m_storage.get().write(sv, {0}); m_addr.append(addr); } @@ -37,14 +41,12 @@ class disk_sync { * value. */ object_handle get_object_handle() { - // TODO: set etag with `to_hex(m_hash.finalize())` return object_handle(std::move(m_addr), m_header_size); } private: - storage::data_view& m_storage; + std::reference_wrapper m_storage; - md5 m_hash; address m_addr; std::size_t m_header_size{0}; }; @@ -58,8 +60,8 @@ class disk_source { disk_source(const disk_source&) = delete; disk_source& operator=(const disk_source&) = delete; - disk_source(disk_source&&) = delete; - disk_source& operator=(disk_source&&) = delete; + disk_source(disk_source&&) = default; + disk_source& operator=(disk_source&&) = default; std::size_t get_header_size() const { return m_objh->header_size(); } @@ -70,10 +72,10 @@ class disk_source { read_size < buffer.size()) { auto frag = m_objh->get_address().get(m_addr_index); - if (m_frag_offset > 0) { - frag.pointer += m_frag_offset; - frag.size -= m_frag_offset; - } + + frag.pointer += m_frag_offset; + frag.size -= m_frag_offset; + if (frag.size + read_size > buffer.size()) { auto remains = buffer.size() - read_size; m_frag_offset += remains; @@ -89,14 +91,14 @@ class disk_source { } if (read_size > 0) { - co_await m_storage.read_address(partial_addr, - {buffer.data(), read_size}); + co_await m_storage.get().read_address(partial_addr, + {buffer.data(), read_size}); } co_return std::span{buffer.data(), read_size}; } private: - storage::data_view& m_storage; + std::reference_wrapper m_storage; std::shared_ptr m_objh; std::size_t m_addr_index{0}; diff --git a/src/proxy/cache/disk/manager.h b/src/proxy/cache/disk/manager.h index ad138b708..cc8f5d688 100644 --- a/src/proxy/cache/disk/manager.h +++ b/src/proxy/cache/disk/manager.h @@ -27,12 +27,7 @@ class manager { using stream = ep::http::stream; using body = ep::http::body; - /* - * Store object handle in cache - * - * It removed address information from the given body. - */ - coro put(object_metadata key, disk_sync& w) { + coro put(object_metadata key, disk_sink& w) { auto objh = w.get_object_handle(); auto obj_size = objh.data_size(); diff --git a/src/proxy/cache/double_buffer_body.h b/src/proxy/cache/double_buffer_body.h deleted file mode 100644 index 5d0d4ba81..000000000 --- a/src/proxy/cache/double_buffer_body.h +++ /dev/null @@ -1,194 +0,0 @@ -#pragma once - -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace boost { -namespace beast { -namespace http { - -/** A Body using a caller provided buffer - - Messages using this body type may be serialized and parsed. - To use this class, the caller must initialize the members - of @ref buffer_body::value_type to appropriate values before - each call to read or write during a stream operation. -*/ - -struct double_buffer_body { - /// The type of the body member when used in a message. - struct value_type { - /** A pointer to a contiguous area of memory of @ref rsize octets, else - `nullptr`. - - @par Only for Parsing - - If this is `nullptr`, the error @ref error::need_buffer - will be returned from @ref parser::put. Otherwise, the - parser will store body octets into the memory pointed to - by `rdata` having `rsize` octets of valid storage. After - octets are stored, the `rdata` and `rsize` members are - adjusted: `rdata` is incremented to point to the next - octet after the rdata written, while `rsize` is decremented - to reflect the remaining space at the memory location - pointed to by `rdata`. - */ - void* rdata = nullptr; - - /** The number of octets in the buffer pointed to by @ref rdata. - - @par Only for Parsing - - The value of this field will be decremented during parsing - to indicate the number of remaining free octets in the - buffer pointed to by `rdata`. When it reaches zero, the - parser will return @ref error::need_buffer, indicating to - the caller that the values of `rdata` and `rsize` should be - updated to point to a new memory buffer. - */ - std::size_t rsize = 0; - - /** A pointer to a contiguous area of memory of @ref wsize octets, else - `nullptr`. - - @par Only for Serializing - - If this is `nullptr` and `more` is `true`, the error - @ref error::need_buffer will be returned from @ref serializer::get - Otherwise, the serializer will use the memory pointed to - by `wdata` having `wsize` octets of valid storage as the - next buffer representing the body. - */ - const void* wdata = nullptr; - - /** The number of octets in the buffer pointed to by @ref wdata. - - @par Only for Serializing - - If `wdata` is `nullptr` during serialization, this value - is ignored. Otherwise, it represents the number of valid - body octets pointed to by `wdata`. - */ - std::size_t wsize = 0; - - /** `true` if this is not the last buffer. - - @par When Serializing - - If this is `true` and `wdata` is `nullptr`, the error - @ref error::need_buffer will be returned from @ref serializer::get - - @par When Parsing - - This field is not used during parsing. - */ - bool more = true; - }; - - /** The algorithm for parsing the body - - Meets the requirements of BodyReader. - */ -#if BOOST_BEAST_DOXYGEN - using reader = __implementation_defined__; -#else - class reader { - value_type& body_; - - public: - template - explicit reader(header&, value_type& b) - : body_(b) {} - - void init(boost::optional const&, error_code& ec) { - ec = {}; - } - - template - std::size_t put(ConstBufferSequence const& buffers, error_code& ec) { - if (!body_.rdata) { - BOOST_BEAST_ASSIGN_EC(ec, error::need_buffer); - return 0; - } - auto const bytes_transferred = net::buffer_copy( - net::buffer(body_.rdata, body_.rsize), buffers); - body_.rdata = static_cast(body_.rdata) + bytes_transferred; - body_.rsize -= bytes_transferred; - if (bytes_transferred == buffer_bytes(buffers)) - ec = {}; - else { - BOOST_BEAST_ASSIGN_EC(ec, error::need_buffer); - } - return bytes_transferred; - } - - void finish(error_code& ec) { ec = {}; } - }; -#endif - - /** The algorithm for serializing the body - - Meets the requirements of BodyWriter. - */ -#if BOOST_BEAST_DOXYGEN - using writer = __implementation_defined__; -#else - class writer { - bool toggle_ = false; - value_type const& body_; - - public: - using const_buffers_type = net::const_buffer; - - template - explicit writer(header const&, value_type const& b) - : body_(b) {} - - void init(error_code& ec) { ec = {}; } - - boost::optional> - get(error_code& ec) { - if (toggle_) { - if (body_.more) { - toggle_ = false; - BOOST_BEAST_ASSIGN_EC(ec, error::need_buffer); - } else { - ec = {}; - } - return boost::none; - } - if (body_.wdata) { - ec = {}; - toggle_ = true; - return { - {const_buffers_type{body_.wdata, body_.wsize}, body_.more}}; - } - if (body_.more) { - BOOST_BEAST_ASSIGN_EC(ec, error::need_buffer); - } else - ec = {}; - return boost::none; - } - }; -#endif -}; - -#if !BOOST_BEAST_DOXYGEN -// operator<< is not supported for double_buffer_body -template -std::ostream& -operator<<(std::ostream& os, - message const& msg) = delete; -#endif - -} // namespace http -} // namespace beast -} // namespace boost diff --git a/src/proxy/handler.cpp b/src/proxy/handler.cpp index 95cee6f34..a02c0004d 100644 --- a/src/proxy/handler.cpp +++ b/src/proxy/handler.cpp @@ -42,7 +42,7 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { constexpr std::size_t buffer_size_to_relay_and_store = 32_MiB; constexpr std::size_t buffer_size_to_relay = 4_KiB; - flat_buffer o_buffer( + flat_buffer buffer( std::max(buffer_size_to_relay, buffer_size_to_relay_and_store)); for (;;) { @@ -111,7 +111,7 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { // TODO timeout response_parser p; response_serializer sr{p.get()}; - co_await async_read_header(outgoing, o_buffer, p); + co_await async_read_header(outgoing, buffer, p); co_await async_write_header(s, sr); } @@ -131,26 +131,28 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { response_serializer sr{p.get()}; LOG_INFO() << peer << ": reading header from downstream"; - co_await async_read_header(outgoing, o_buffer, p); + co_await async_read_header(outgoing, buffer, p); if (r.method() == verb::head) { LOG_INFO() << peer << ": HEAD request, skipping body relay"; co_await async_write_header(s, sr); } else if (get_object::can_handle(*req)) { - auto d_sync = cache::disk::disk_sync{m_dv}; - auto s_sync = socket_sync{s}; + auto d_sink = cache::disk::disk_sink{m_dv}; + auto s_sink = socket_sink{s}; auto body_size = get_content_length(p.get()); if (!body_size.has_value()) { throw std::runtime_error("no content length"); } co_await async_read( - outgoing, o_buffer, *body_size, tee_sync(s_sync, d_sync), + outgoing, buffer, *body_size, tee(s_sink, d_sink), [&]() -> coro { - co_await async_write_header(s, sr, d_sync); + auto n = co_await async_write_header( + tee(s_sink, d_sink), sr); + d_sink.set_header_size(n); }); co_await m_mgr.put( - cache::disk::object_metadata{req->object_key()}, d_sync); + cache::disk::object_metadata{req->object_key()}, d_sink); } else { auto body_size = get_content_length(p.get()); @@ -158,7 +160,7 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { throw std::runtime_error("no content length"); } co_await async_read( - outgoing, o_buffer, *body_size, socket_sync(s), + outgoing, buffer, *body_size, socket_sink(s), [&]() -> coro { co_await async_write_header(s, sr); }); diff --git a/src/proxy/http.h b/src/proxy/http.h index c54b0f05e..0b49d0533 100644 --- a/src/proxy/http.h +++ b/src/proxy/http.h @@ -121,9 +121,8 @@ coro async_read_header(const SourceType& source, Parser& parser) { } } -template -coro async_write_header(ServerSocketType& server_socket, - Serializer& sr, SyncType& sync) { +template +coro async_write_header(SinkType&& sink, Serializer& sr) { using boost::asio::experimental::awaitable_operators::operator&&; std::ostringstream oss; boost::system::error_code ec; @@ -133,24 +132,23 @@ coro async_write_header(ServerSocketType& server_socket, if (header_str.size() == 0) { throw std::runtime_error("Could not serialize header"); } - co_await (sync.put(header_str) && [&]() -> coro { - co_await async_write(server_socket, boost::asio::buffer(header_str)); - }()); - sync.set_header_size(header_str.size()); + co_await sink.put(header_str); co_return header_str.size(); } -template +template coro async_read( - Incomming& in, boost::beast::flat_buffer& b, std::size_t payload_size, - SyncType&& sync, + Incomming& s, boost::beast::flat_buffer& b, std::size_t payload_size, + SinkType&& sink, std::function()> precursor = []() -> coro { co_return; }) { using boost::asio::experimental::awaitable_operators::operator&&; + auto sink_ref = std::forward(sink); + if (b.data().size() >= payload_size) { auto sv = std::span( static_cast(b.data().data()), payload_size); - co_await (sync.put(sv) && precursor()); + co_await (sink_ref.put(sv) && precursor()); b.consume(sv.size()); } else { @@ -163,7 +161,7 @@ coro async_read( auto* rbuf = &b; auto* wbuf = &b2; - for (auto n = co_await (read(in, *rbuf, + for (auto n = co_await (read(s, *rbuf, std::min(payload_size, chunk_size) - rbuf->data().size()) && precursor()); @@ -175,17 +173,17 @@ coro async_read( } payload_size -= wbuf->data().size(); auto new_n = co_await ( - read(in, *rbuf, std::min(payload_size, chunk_size)) && - sync.put(get_span(wbuf->data()))); + read(s, *rbuf, std::min(payload_size, chunk_size)) && + sink_ref.put(get_span(wbuf->data()))); wbuf->consume(wbuf->data().size()); n = new_n; } } else { - auto n = co_await (read(in, b, payload_size - b.data().size()) && + auto n = co_await (read(s, b, payload_size - b.data().size()) && precursor()); b.commit(n); - co_await sync.put(get_span(b.data())); + co_await sink_ref.put(get_span(b.data())); b.consume(b.data().size()); } } @@ -199,17 +197,20 @@ coro async_write( std::function()> precursor = []() -> coro { co_return; }) { using boost::asio::experimental::awaitable_operators::operator&&; + auto source_ref = std::forward(source); + char _buf[2][buffer_size]; char* rbuf = _buf[0]; char* wbuf = _buf[1]; - for (auto data = co_await (source.get({rbuf, buffer_size}) && precursor()); + for (auto data = + co_await (source_ref.get({rbuf, buffer_size}) && precursor()); !data.empty();) { std::swap(rbuf, wbuf); - auto d = - co_await (source.get({rbuf, buffer_size}) && [&]() -> coro { - co_await async_write(s, boost::asio::const_buffer(data)); - }()); + auto d = co_await (source_ref.get({rbuf, buffer_size}) && + [&]() -> coro { + co_await async_write(s, boost::asio::const_buffer(data)); + }()); data = d; } } diff --git a/src/proxy/socket_io.h b/src/proxy/socket_io.h index 4d8fdf31c..ec73680f4 100644 --- a/src/proxy/socket_io.h +++ b/src/proxy/socket_io.h @@ -7,9 +7,9 @@ namespace uh::cluster::proxy { -template class socket_sync { +template class socket_sink { public: - socket_sync(SocketType& s) + socket_sink(SocketType& s) : m_s{s} {} coro put(std::span sv) { diff --git a/src/proxy/tee_io.h b/src/proxy/tee_io.h index 088974e0d..6cba1c8f2 100644 --- a/src/proxy/tee_io.h +++ b/src/proxy/tee_io.h @@ -5,9 +5,9 @@ namespace uh::cluster::proxy { -template class tee_sync { +template class tee { public: - tee_sync(T& t, U& u) + tee(T& t, U& u) : m_t{t}, m_u{u} {} diff --git a/test/unit/test_disk_cache_body.cpp b/test/unit/test_disk_cache_body.cpp index fccd63ebb..060584cd3 100644 --- a/test/unit/test_disk_cache_body.cpp +++ b/test/unit/test_disk_cache_body.cpp @@ -99,21 +99,22 @@ BOOST_AUTO_TEST_CASE(goes_with_relay_store_body) { parser p; serializer sr{p.get()}; - disk_sync dsync(data_view); - socket_sync ssync(server_socket); + disk_sink dsink(data_view); + socket_sink ssink(server_socket); co_spawn( m_ioc, [&]() -> coro { auto n = co_await async_read_header(server_socket, b, p); - auto m = co_await async_write_header(server_socket, sr, dsync); + auto m = co_await async_write_header(tee(dsink, ssink), sr); + dsink.set_header_size(m); BOOST_TEST(n == m); auto body_size = get_content_length(p.get()); if (!body_size.has_value()) { throw std::runtime_error("no content length"); } co_await async_read<1_KiB>(server_socket, b, *body_size, - tee_sync(dsync, ssync)); + tee(dsink, ssink)); }, boost::asio::use_future) .get(); @@ -134,7 +135,7 @@ BOOST_AUTO_TEST_CASE(goes_with_relay_store_body) { BOOST_TEST(output_str == std::string_view(raw_message.data(), raw_message.size())); - auto objh = dsync.get_object_handle(); + auto objh = dsink.get_object_handle(); BOOST_TEST(objh.data_size() == raw_message.size()); std::vector buf(raw_message.size()); @@ -195,7 +196,7 @@ BOOST_AUTO_TEST_CASE(test_relay_body) { throw std::runtime_error("no content length"); } co_await async_read<1_KiB>(server_socket, b, *body_size, - socket_sync(server_socket)); + socket_sink(server_socket)); }, boost::asio::use_future) .get(); From 86a4df8f229ad4017556a33e35059732baf36d56 Mon Sep 17 00:00:00 2001 From: Sungsik Date: Tue, 23 Sep 2025 15:55:16 +0200 Subject: [PATCH 32/35] Handle when buffer is filled more than chunk_size --- src/proxy/http.h | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/src/proxy/http.h b/src/proxy/http.h index 0b49d0533..4f3f6bb63 100644 --- a/src/proxy/http.h +++ b/src/proxy/http.h @@ -146,9 +146,10 @@ coro async_read( auto sink_ref = std::forward(sink); if (b.data().size() >= payload_size) { + co_await precursor(); auto sv = std::span( static_cast(b.data().data()), payload_size); - co_await (sink_ref.put(sv) && precursor()); + co_await sink_ref.put(sv); b.consume(sv.size()); } else { @@ -161,24 +162,26 @@ coro async_read( auto* rbuf = &b; auto* wbuf = &b2; - for (auto n = co_await (read(s, *rbuf, - std::min(payload_size, chunk_size) - - rbuf->data().size()) && - precursor()); - n != 0;) { - std::swap(rbuf, wbuf); - wbuf->commit(n); - if (wbuf->data().size() != std::min(payload_size, chunk_size)) { - throw std::runtime_error("buffer size mismatch"); - } - payload_size -= wbuf->data().size(); - auto new_n = co_await ( - read(s, *rbuf, std::min(payload_size, chunk_size)) && - sink_ref.put(get_span(wbuf->data()))); + auto remained = payload_size; + std::size_t n = 0; + if (chunk_size > rbuf->data().size()) { + n = co_await ( + read(s, *rbuf, chunk_size - rbuf->data().size()) && + precursor()); + rbuf->commit(n); + remained -= rbuf->data().size(); + } else { + co_await precursor(); + } + do { + std::swap(rbuf, wbuf); + n = co_await (read(s, *rbuf, std::min(remained, chunk_size)) && + sink_ref.put(get_span(wbuf->data()))); + rbuf->commit(n); + remained -= rbuf->data().size(); wbuf->consume(wbuf->data().size()); - n = new_n; - } + } while (n != 0); } else { auto n = co_await (read(s, b, payload_size - b.data().size()) && precursor()); From b7ee8c097e63b8a940cf6e1d88ada8bf622501ea Mon Sep 17 00:00:00 2001 From: Sungsik Date: Tue, 23 Sep 2025 16:19:48 +0200 Subject: [PATCH 33/35] Fix compile error --- test/unit/test_disk_cache_manager.cpp | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/test/unit/test_disk_cache_manager.cpp b/test/unit/test_disk_cache_manager.cpp index a64a14a7c..9933a0a94 100644 --- a/test/unit/test_disk_cache_manager.cpp +++ b/test/unit/test_disk_cache_manager.cpp @@ -15,10 +15,10 @@ BOOST_AUTO_TEST_CASE(put_and_get_with_metadata) { manager mgr{manager::create(m_ioc, data_view, 256)}; std::string data = random_string(64); - writer w(data_view); + disk_sink sink(data_view); boost::asio::co_spawn( - m_ioc, w.put(std::span(data.data(), data.size())), + m_ioc, sink.put(std::span(data.data(), data.size())), boost::asio::use_future) .get(); @@ -26,15 +26,15 @@ BOOST_AUTO_TEST_CASE(put_and_get_with_metadata) { key.path = "/foo/bar"; key.version = "v1"; - boost::asio::co_spawn(m_ioc, mgr.put(key, w), boost::asio::use_future) + boost::asio::co_spawn(m_ioc, mgr.put(key, sink), boost::asio::use_future) .get(); - auto writer = mgr.get(key); - BOOST_TEST(writer != nullptr); + auto source = mgr.get(key); + BOOST_TEST(source != nullptr); auto buf = std::string(128, '\0'); auto sv = - boost::asio::co_spawn(m_ioc, writer->get(buf), boost::asio::use_future) + boost::asio::co_spawn(m_ioc, source->get(buf), boost::asio::use_future) .get(); BOOST_TEST(sv.size() == data.size()); @@ -51,9 +51,9 @@ BOOST_AUTO_TEST_CASE(eviction_test) { std::string data = random_string(32); datas.push_back(data); - writer w(data_view); + disk_sink sink(data_view); boost::asio::co_spawn( - m_ioc, w.put(std::span(data.data(), data.size())), + m_ioc, sink.put(std::span(data.data(), data.size())), boost::asio::use_future) .get(); @@ -62,12 +62,13 @@ BOOST_AUTO_TEST_CASE(eviction_test) { key.version = "v" + std::to_string(i); keys.push_back(key); - boost::asio::co_spawn(m_ioc, mgr.put(key, w), boost::asio::use_future) + boost::asio::co_spawn(m_ioc, mgr.put(key, sink), + boost::asio::use_future) .get(); } - auto writer = mgr.get(keys.front()); - BOOST_TEST(writer == nullptr); + auto source = mgr.get(keys.front()); + BOOST_TEST(source == nullptr); } BOOST_AUTO_TEST_SUITE_END() From 82e8e76c354293aa95b41ac6e1bb7128d1a73655 Mon Sep 17 00:00:00 2001 From: Sungsik Date: Wed, 24 Sep 2025 13:42:29 +0200 Subject: [PATCH 34/35] Move precursor's position to the first --- src/common/types/common_types.h | 15 +++++++ src/proxy/handler.cpp | 19 +++++---- src/proxy/http.h | 73 +++++++++++++++++++++++++-------- 3 files changed, 82 insertions(+), 25 deletions(-) diff --git a/src/common/types/common_types.h b/src/common/types/common_types.h index 3d87ea1e9..46432833b 100644 --- a/src/common/types/common_types.h +++ b/src/common/types/common_types.h @@ -37,6 +37,21 @@ struct refcount_t { using utc_time = std::chrono::time_point; template using coro = boost::asio::traced_awaitable; +inline coro async_noop() { co_return; }; + +template struct is_boost_awaitable : std::false_type {}; + +template +struct is_boost_awaitable> : std::true_type {}; + +template +constexpr bool is_boost_awaitable_v = is_boost_awaitable::value; + +template +requires is_boost_awaitable_v> +inline coro async_wrap(Awaitable&& v) { + co_await std::move(v); +}; inline thread_local opentelemetry::context::Context THREAD_LOCAL_CONTEXT; diff --git a/src/proxy/handler.cpp b/src/proxy/handler.cpp index a02c0004d..f8e1ae5e0 100644 --- a/src/proxy/handler.cpp +++ b/src/proxy/handler.cpp @@ -93,9 +93,9 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { parser.get().set(field::via, via_value); co_await async_write( - s, *d_source, [&]() -> coro { - co_await async_write_header(s, serializer); - }); + async_write_header(s, serializer, + boost::asio::use_awaitable), + s, *d_source); LOG_INFO() << peer << ": cache result served"; continue; @@ -144,13 +144,15 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { if (!body_size.has_value()) { throw std::runtime_error("no content length"); } + LOG_INFO() << peer << ": relaying and storing body of size " + << *body_size; co_await async_read( - outgoing, buffer, *body_size, tee(s_sink, d_sink), [&]() -> coro { auto n = co_await async_write_header( tee(s_sink, d_sink), sr); d_sink.set_header_size(n); - }); + }, + outgoing, buffer, *body_size, tee(s_sink, d_sink)); co_await m_mgr.put( cache::disk::object_metadata{req->object_key()}, d_sink); @@ -159,11 +161,10 @@ coro handler::handle(boost::asio::ip::tcp::socket s) { if (!body_size.has_value()) { throw std::runtime_error("no content length"); } + LOG_INFO() << peer << ": relaying body of size " << *body_size; co_await async_read( - outgoing, buffer, *body_size, socket_sink(s), - [&]() -> coro { - co_await async_write_header(s, sr); - }); + async_write_header(s, sr, boost::asio::use_awaitable), + outgoing, buffer, *body_size, socket_sink(s)); } LOG_INFO() << peer << ": done"; diff --git a/src/proxy/http.h b/src/proxy/http.h index 4f3f6bb63..df17e1747 100644 --- a/src/proxy/http.h +++ b/src/proxy/http.h @@ -137,16 +137,38 @@ coro async_write_header(SinkType&& sink, Serializer& sr) { } template -coro async_read( - Incomming& s, boost::beast::flat_buffer& b, std::size_t payload_size, - SinkType&& sink, - std::function()> precursor = []() -> coro { co_return; }) { +coro async_read(Incomming& s, boost::beast::flat_buffer& b, + std::size_t payload_size, SinkType&& sink) { + co_await async_read(async_noop(), s, b, payload_size, + std::forward(sink)); +} + +template +coro async_read(Awaitable&& precursor, Incomming& s, + boost::beast::flat_buffer& b, std::size_t payload_size, + SinkType&& sink) { using boost::asio::experimental::awaitable_operators::operator&&; auto sink_ref = std::forward(sink); + using precursor_type = std::decay_t; + coro precursor_wrapper; + + if constexpr (std::is_same_v>) { + precursor_wrapper = std::move(precursor); + } else if constexpr (is_boost_awaitable_v) { + precursor_wrapper = async_wrap(std::move(precursor)); + } else if constexpr (std::is_invocable_r_v, precursor_type>) { + precursor_wrapper = precursor(); + } else { + throw std::runtime_error( + "invalid precursor type: " + + boost::core::demangle(typeid(precursor_type).name())); + } + if (b.data().size() >= payload_size) { - co_await precursor(); + co_await std::move(precursor_wrapper); auto sv = std::span( static_cast(b.data().data()), payload_size); co_await sink_ref.put(sv); @@ -167,11 +189,11 @@ coro async_read( if (chunk_size > rbuf->data().size()) { n = co_await ( read(s, *rbuf, chunk_size - rbuf->data().size()) && - precursor()); + std::move(precursor_wrapper)); rbuf->commit(n); remained -= rbuf->data().size(); } else { - co_await precursor(); + co_await std::move(precursor_wrapper); } do { @@ -184,7 +206,7 @@ coro async_read( } while (n != 0); } else { auto n = co_await (read(s, b, payload_size - b.data().size()) && - precursor()); + std::move(precursor_wrapper)); b.commit(n); co_await sink_ref.put(get_span(b.data())); b.consume(b.data().size()); @@ -194,23 +216,42 @@ coro async_read( b.shrink_to_fit(); } -template -coro async_write( - SocketType& s, SourceType& source, - std::function()> precursor = []() -> coro { co_return; }) { +template +coro async_write(SocketType& s, SourceType& source) { + co_await async_write(async_noop(), s, source); +} +template +coro async_write(Awaitable&& precursor, SocketType& s, + SourceType& source) { using boost::asio::experimental::awaitable_operators::operator&&; auto source_ref = std::forward(source); - char _buf[2][buffer_size]; + using precursor_type = std::decay_t; + coro precursor_wrapper; + + if constexpr (std::is_same_v>) { + precursor_wrapper = std::move(precursor); + } else if constexpr (is_boost_awaitable_v) { + precursor_wrapper = async_wrap(std::move(precursor)); + } else if constexpr (std::is_invocable_r_v, precursor_type>) { + precursor_wrapper = precursor(); + } else { + throw std::runtime_error( + "invalid precursor type: " + + boost::core::demangle(typeid(precursor_type).name())); + } + + char _buf[2][chunk_size]; char* rbuf = _buf[0]; char* wbuf = _buf[1]; - for (auto data = - co_await (source_ref.get({rbuf, buffer_size}) && precursor()); + for (auto data = co_await (source_ref.get({rbuf, chunk_size}) && + std::move(precursor_wrapper)); !data.empty();) { std::swap(rbuf, wbuf); - auto d = co_await (source_ref.get({rbuf, buffer_size}) && + auto d = co_await (source_ref.get({rbuf, chunk_size}) && [&]() -> coro { co_await async_write(s, boost::asio::const_buffer(data)); }()); From 8c000e439787a0ee60d4ab4d5e7225715d1998aa Mon Sep 17 00:00:00 2001 From: Sungsik Date: Wed, 24 Sep 2025 14:36:38 +0200 Subject: [PATCH 35/35] Reorder template functions --- src/proxy/http.h | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/proxy/http.h b/src/proxy/http.h index df17e1747..23a9c016b 100644 --- a/src/proxy/http.h +++ b/src/proxy/http.h @@ -136,13 +136,6 @@ coro async_write_header(SinkType&& sink, Serializer& sr) { co_return header_str.size(); } -template -coro async_read(Incomming& s, boost::beast::flat_buffer& b, - std::size_t payload_size, SinkType&& sink) { - co_await async_read(async_noop(), s, b, payload_size, - std::forward(sink)); -} - template coro async_read(Awaitable&& precursor, Incomming& s, @@ -216,10 +209,13 @@ coro async_read(Awaitable&& precursor, Incomming& s, b.shrink_to_fit(); } -template -coro async_write(SocketType& s, SourceType& source) { - co_await async_write(async_noop(), s, source); +template +coro async_read(Incomming& s, boost::beast::flat_buffer& b, + std::size_t payload_size, SinkType&& sink) { + co_await async_read(async_noop(), s, b, payload_size, + std::forward(sink)); } + template coro async_write(Awaitable&& precursor, SocketType& s, @@ -259,4 +255,9 @@ coro async_write(Awaitable&& precursor, SocketType& s, } } +template +coro async_write(SocketType& s, SourceType& source) { + co_await async_write(async_noop(), s, source); +} + } // namespace uh::cluster::proxy