diff --git a/src/ipc/libmultiprocess/test/mp/test/listen_tests.cpp b/src/ipc/libmultiprocess/test/mp/test/listen_tests.cpp index 4e579d99d5e..d7e9e0cc18e 100644 --- a/src/ipc/libmultiprocess/test/mp/test/listen_tests.cpp +++ b/src/ipc/libmultiprocess/test/mp/test/listen_tests.cpp @@ -129,14 +129,40 @@ public: std::thread thread; }; +//! ConnectionReceiver wrapper whose first accept() fails with the given +//! exception type, like kj does for an accept() failing with EMFILE +//! (OVERLOADED), and which forwards later calls to the real receiver. +class FailFirstAccept final : public kj::ConnectionReceiver +{ +public: + FailFirstAccept(kj::Own inner, kj::Exception::Type type) + : m_inner(kj::mv(inner)), m_type(type) {} + + kj::Promise> accept() override + { + if (m_fail) { + m_fail = false; + return kj::Exception(m_type, __FILE__, __LINE__, kj::str("simulated accept failure")); + } + return m_inner->accept(); + } + + uint getPort() override { return m_inner->getPort(); } + + kj::Own m_inner; + kj::Exception::Type m_type; + bool m_fail{true}; +}; + //! Runs a server EventLoop on its own thread, starts ListenConnections() on a //! UnixListener socket, and records connection/disconnection counts through //! EventLoop test hooks class ListenSetup { public: - explicit ListenSetup(std::optional max_connections = std::nullopt) - : thread([this, max_connections] { + explicit ListenSetup(std::optional max_connections = std::nullopt, + std::optional fail_first_accept = std::nullopt) + : thread([this, max_connections, fail_first_accept] { EventLoop loop("mptest-server", [](mp::LogMessage log) { KJ_LOG(INFO, log.level, log.message); if (log.level == mp::Log::Raise) throw std::runtime_error(log.message); @@ -153,7 +179,15 @@ public: }; m_loop_ref.emplace(loop); FooImplementation foo; - ListenConnections(loop, listener.release(), foo, max_connections); + if (fail_first_accept) { + auto receiver{loop.m_io_context.lowLevelProvider->wrapListenSocketFd( + listener.release(), kj::LowLevelAsyncIoProvider::TAKE_OWNERSHIP)}; + _Listen( + std::make_shared(kj::heap(kj::mv(receiver), *fail_first_accept), max_connections), + loop, foo); + } else { + ListenConnections(loop, listener.release(), foo, max_connections); + } ready_promise.set_value(); loop.loop(); }) @@ -289,6 +323,17 @@ KJ_TEST("ListenConnections accepts multiple connections") KJ_EXPECT(client3->client->add(3, 4) == 7); } +KJ_TEST("ListenConnections keeps accepting after a temporary accept failure") +{ + // The first accept() fails as if the process were out of file + // descriptors. The listener should retry and serve the client once the + // failure clears. + ListenSetup server(std::nullopt, kj::Exception::Type::OVERLOADED); + auto client = std::make_unique(server.listener.MakeConnectedSocket()); + server.WaitForConnectedCount(1); + KJ_EXPECT(client->client->add(1, 2) == 3); +} + } // namespace } // namespace test } // namespace mp