yams/tests/unit/daemon/request_handler_lifetime_catch2_test.cpp
Trevon 4e97b31fef
Some checks failed
Fuzz Smoke / libFuzzer smoke (push) Has been cancelled
chore(warnings): clean up compiler warnings on GCC, Clang and Apple Clang (#279)
* chore(warnings): default-initialize aggregate members left out of brace-init

GCC -Wmissing-field-initializers fired about 1300 times where tests and
src build structs with a short positional brace list. Give the omitted
members an empty default member initializer. Value-initialization from
{} is what aggregate init already did for them, so behavior is
unchanged.

Signed-off-by: trvon <me@trevon.dev>

* chore(warnings): remove or annotate unused parameters, functions and variables

- init_command, s3_signer test, request_handler_lifetime test: comment out
  the unused parameter name.
- video_content_handler: path is only read under YAMS_HAVE_FFPROBE.
- vector_memory_scaling_bench: main() takes no arguments.
- daemon_status_render: delete unused neutralText helper.
- DaemonMetrics: mallocStackLoggingNoCompactEnabled is only called in the
  __APPLE__ block, so define it there.
- SocketServer: einval_streak only matters in the __APPLE__ EINVAL path.
- onnx_plugin_harness test, vector_backend_engine_compare bench,
  work_coordinator test: mark platform-conditional or busy-loop
  variables [[maybe_unused]].

Signed-off-by: trvon <git@trevon.dev>

* chore(warnings): fix sign-compare, range-loop, nodiscard and dangling-ref in tests

- search_admission_controller: count started workers in std::uint32_t so
  the comparison with the uint32 cap is same-signed.
- p2p_manager: iterate the invalid-host list by const reference.
- integrity_verifier: discard the nodiscard results the tests do not
  inspect with static_cast<void>.
- post_ingest_queue: findNode/findEdge take std::string_view so no
  temporary std::string is created for the call (GCC
  -Wdangling-reference false positive on the returned reference).
- topology_ablation_bench: the doc comment ended a line with a backslash,
  which continues the comment (-Wcomment); show the env export on its own
  line.

Signed-off-by: trvon <git@trevon.dev>

* chore(warnings): drop redundant std::move of a const captured json

The coroutine lambda is not mutable, so the captured optional<json> is
const and std::move(*progressToken) already selected the copy assignment.
Drop the std::move so the code states what actually happens.

Signed-off-by: trvon <git@trevon.dev>

* chore(warnings): mark __int128 use in db_integrity_stamp as an extension

-Wpedantic flags __int128. Use a __extension__ typedef, the GCC/Clang
idiom for an intentional extension, instead of the bare keyword.

Signed-off-by: trvon <git@trevon.dev>

* chore(warnings): remove always-true unsigned >= 0 comparisons

GCC -Wtype-limits flagged 51 test assertions of the form
CHECK(unsignedValue >= 0) that can never fail, plus a dead
`totalCount.value() < 0` clause in the graph listByType handler (the
count is std::size_t). In the tests keep the evaluation as a
static_cast<void> so the accessors are still exercised; in the handler
drop the dead clause and keep the range check.

Signed-off-by: trvon <me@trevon.dev>

---------

Signed-off-by: trvon <me@trevon.dev>
Signed-off-by: trvon <git@trevon.dev>
2026-10-08 14:30:28 -06:00

138 lines
4.8 KiB
C++

// Copyright (c) 2025 YAMS Contributors
// SPDX-License-Identifier: GPL-3.0-or-later
#include <catch2/catch_test_macros.hpp>
#include <yams/daemon/ipc/request_handler.h>
#include <boost/asio.hpp>
#include <boost/asio/experimental/awaitable_operators.hpp>
#include <chrono>
#include <future>
#include <memory>
#include <optional>
#include <thread>
using namespace yams::daemon;
using namespace boost::asio;
namespace {
class MockRequestProcessor : public RequestProcessor {
public:
boost::asio::awaitable<Response> process(const Request& request) override {
(void)request;
co_return SuccessResponse{"OK"};
}
boost::asio::awaitable<std::optional<Response>>
process_streaming(const Request& /*request*/) override {
// Simulate long running operation
boost::asio::steady_timer timer(co_await boost::asio::this_coro::executor);
timer.expires_after(std::chrono::milliseconds(100));
co_await timer.async_wait(boost::asio::use_awaitable);
co_return SuccessResponse{"Streamed"};
}
bool supports_streaming(const Request& request) const override {
(void)request;
return true;
}
};
} // anonymous namespace
TEST_CASE("RequestHandlerLifetime: detached coroutine survives handler destruction",
"[daemon][.streaming_disabled]") {
#ifdef _WIN32
SKIP("Unix domain socket tests skipped on Windows");
#endif
// Streaming path is currently disabled in request_handler.cpp (force_unary=true)
// to avoid head-of-line blocking. This test requires the streaming path to spawn
// detached coroutines. Skip until streaming is re-enabled.
SKIP("Streaming path disabled (force_unary=true in request_handler.cpp)");
boost::asio::io_context io;
// Create a socket pair
boost::asio::local::stream_protocol::socket client_sock(io);
boost::asio::local::stream_protocol::socket server_sock(io);
boost::asio::local::connect_pair(client_sock, server_sock);
auto server_sock_ptr =
std::make_shared<boost::asio::local::stream_protocol::socket>(std::move(server_sock));
// Create handler
RequestHandler::Config config;
config.enable_multiplexing = true;
config.enable_streaming = true;
auto processor = std::make_shared<MockRequestProcessor>();
auto handler = std::make_shared<RequestHandler>(processor, config);
yams::compat::stop_source stop_source;
// Spawn the handler connection loop
std::promise<void> handler_finished;
auto handler_future = handler_finished.get_future();
boost::asio::co_spawn(
io,
[&, handler, server_sock_ptr,
token = stop_source.get_token()]() mutable -> boost::asio::awaitable<void> {
co_await handler->handle_connection(server_sock_ptr, token, 1);
handler_finished.set_value();
},
boost::asio::detached);
// Send a request that triggers streaming (and thus detached coroutine)
// We need to construct a valid message frame
Request req = SearchRequest{}; // SearchRequest supports streaming
Message msg;
msg.version = 1;
msg.requestId = 123;
msg.expectsStreamingResponse = true;
msg.payload = req;
MessageFramer framer(1024 * 1024);
std::vector<uint8_t> frame;
auto res = framer.frame_message_into(msg, frame);
REQUIRE(res);
boost::asio::write(client_sock, boost::asio::buffer(frame));
// Run IO for a bit to let the request be received and detached coroutine spawned
io.run_for(std::chrono::milliseconds(10));
// Now close the server socket to force handle_connection to exit
server_sock_ptr->close();
// Run IO until handle_connection finishes
io.run_for(std::chrono::milliseconds(10));
// Verify handler_connection finished
REQUIRE(handler_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready);
// Drop the main handler shared_ptr
std::weak_ptr<RequestHandler> weak_handler = handler;
handler.reset();
// At this point, if the detached coroutine captured shared_from_this,
// weak_handler should still be lockable (or at least the object alive).
// But wait, the detached coroutine might have finished if the socket close caused write error.
// However, the MockRequestProcessor waits 100ms.
// We only ran for 20ms total.
// So the detached coroutine should still be waiting on the timer.
// If the fix is working, the handler should still be alive because the detached coroutine holds
// a shared_ptr.
REQUIRE_FALSE(weak_handler.expired());
INFO("RequestHandler should be kept alive by detached coroutine");
// Run IO to let the detached coroutine finish (timer expires)
io.run_for(std::chrono::milliseconds(200));
// Now the detached coroutine should be done, and handler destroyed
REQUIRE(weak_handler.expired());
INFO("RequestHandler should be destroyed after detached coroutine finishes");
}