// Copyright (c) 2025 YAMS Contributors // SPDX-License-Identifier: GPL-3.0-or-later #include #include #include #include #include #include #include #include #include using namespace yams::daemon; using namespace boost::asio; namespace { class MockRequestProcessor : public RequestProcessor { public: boost::asio::awaitable process(const Request& request) override { (void)request; co_return SuccessResponse{"OK"}; } boost::asio::awaitable> 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(std::move(server_sock)); // Create handler RequestHandler::Config config; config.enable_multiplexing = true; config.enable_streaming = true; auto processor = std::make_shared(); auto handler = std::make_shared(processor, config); yams::compat::stop_source stop_source; // Spawn the handler connection loop std::promise 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 { 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 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 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"); }