Loading test/idle_reaper_test.cpp +126 −3 Original line number Diff line number Diff line Loading @@ -56,6 +56,7 @@ // sweepIntervalSeconds()). It must be set before the first sweep attempt // of the process, so it's set at the very top of main(). #include <atomic> #include <iostream> #include <string> #include <cstring> Loading Loading @@ -97,12 +98,43 @@ class IdleReaperServer : public event { } }; // Server variant for scenario 3 below: captures the con& handed to ConnectEvent so the test // can reach into ConSlot::PendingOps directly (see connection.h) -- the same field a // background offload task (e.g. libhttppp's H2 dispatch pool) increments/decrements around // its own work, and that reapIdleConnections() now checks before closing a connection purely // by lasteventime age. class PendingOpsReaperServer : public event { public: std::atomic<con*> lastConnected{nullptr}; PendingOpsReaperServer(std::vector<netplus::socket*> serversockets, int timeout, int idleTimeoutSeconds) : event(serversockets, timeout, idleTimeoutSeconds) { } void RequestEvent(con &curcon, const int tid, ULONG_PTR args) override { static const std::string body = "still-alive\n"; curcon.SendData.append(body.data(), body.size()); curcon.RecvData.clear(); } void ResponseEvent(con &curcon, const int tid, ULONG_PTR args) override {} void ConnectEvent(con &curcon, const int tid, ULONG_PTR args) override { lastConnected = &curcon; } void DisconnectEvent(con &curcon, const int tid, ULONG_PTR args) override {} void CreateConnection(std::shared_ptr<con> &res) override { res = std::make_shared<con>(this); } }; // Runs `srv`'s event loop on a background thread until `body` returns, // then stops it and joins. Centralizes the event::Running dance (a static // flag shared by every netplus::event instance in the process, so the two // flag shared by every netplus::event instance in the process, so the // scenarios below must run strictly sequentially, never concurrently). template <typename Fn> bool withRunningServer(IdleReaperServer &srv, Fn body) { template <typename ServerT, typename Fn> bool withRunningServer(ServerT &srv, Fn body) { event::Running = true; std::thread serverThread([&srv]() { Loading Loading @@ -232,6 +264,97 @@ bool testDefaultDisabledLeavesConnectionAlive() { }); } // Scenario 3: a connection with ConSlot::PendingOps > 0 must NOT be reaped no matter how long // lasteventime has aged -- this is the guard added after a real proxyplus incident where the // reaper closed a connection out from under a still-running background dispatch task (which // doesn't refresh lasteventime), sending a local FIN while the peer was still legitimately // waiting on a response and producing sockets stuck in FIN_WAIT_2 under load. Also proves the // guard is transient, not a permanent opt-out: once PendingOps returns to 0, the very next // sweep reaps the connection exactly like scenario 1. bool testPendingOpsBlocksReap() { std::cout << "[idle_reaper_test] scenario 3: PendingOps > 0 blocks the idle reaper from " "closing a connection despite lasteventime being expired" << std::endl; const int idleTimeoutSeconds = 2; tcp serverSocket("127.0.0.1", 18087, 1024, -1); PendingOpsReaperServer srv({&serverSocket}, /*timeout(ms)=*/200, idleTimeoutSeconds); return withRunningServer(srv, [&]() -> bool { tcp client; client.connect("127.0.0.1", 18087); client.setTimeout(5000); con *serverCon = nullptr; for (int i = 0; i < 50 && !serverCon; ++i) { serverCon = srv.lastConnected.load(); if (!serverCon) std::this_thread::sleep_for(std::chrono::milliseconds(100)); } if (!serverCon) { std::cerr << " FAIL: ConnectEvent never fired" << std::endl; client.close(); return false; } // Simulate a background offload task still in flight on this connection -- exactly // ConSlot::PendingOps's intended use (see connection.h). serverCon->slots[0].PendingOps.fetch_add(1); // Wait past idleTimeoutSeconds + the (overridden) 1s sweep throttle, same margin as // scenario 1 -- long enough that, without the PendingOps guard, this connection would // already have been reaped. std::this_thread::sleep_for(std::chrono::seconds(idleTimeoutSeconds + 4)); bool stillAlive = false; try { const std::string request = "ping"; buffer sendbuf(request.data(), request.size()); client.sendData(sendbuf); buffer recvbuf(64); size_t n = client.recvData(recvbuf); std::string response(recvbuf.data.buf, n); stillAlive = response.find("still-alive") != std::string::npos; if (!stillAlive) std::cerr << " FAIL: unexpected response while PendingOps > 0: " << response << std::endl; } catch (NetException &e) { std::cerr << " FAIL: connection was closed despite PendingOps > 0: " << e.what() << std::endl; } if (!stillAlive) { client.close(); return false; } std::cout << " OK: connection survived the idle sweep while PendingOps > 0" << std::endl; // Release the guard and prove it's transient: the request/response above just // refreshed lasteventime, so wait a full new idle window from here before expecting // the next sweep to reap it now that PendingOps is back to 0. serverCon->slots[0].PendingOps.fetch_sub(1); std::this_thread::sleep_for(std::chrono::seconds(idleTimeoutSeconds + 4)); buffer recvbuf2(64); try { size_t n = client.recvData(recvbuf2); std::cerr << " FAIL: expected the server to reap this connection once PendingOps " "returned to 0, but recvData returned " << n << " bytes instead of EOF" << std::endl; client.close(); return false; } catch (NetException &e) { client.close(); if (e.getErrorType() == NetException::Error) { std::cout << " OK: connection reaped on the next sweep once PendingOps " "returned to 0" << std::endl; return true; } std::cerr << " FAIL: recvData threw, but not a peer-closed error: " << e.what() << std::endl; return false; } }); } } // namespace int main() { Loading Loading
test/idle_reaper_test.cpp +126 −3 Original line number Diff line number Diff line Loading @@ -56,6 +56,7 @@ // sweepIntervalSeconds()). It must be set before the first sweep attempt // of the process, so it's set at the very top of main(). #include <atomic> #include <iostream> #include <string> #include <cstring> Loading Loading @@ -97,12 +98,43 @@ class IdleReaperServer : public event { } }; // Server variant for scenario 3 below: captures the con& handed to ConnectEvent so the test // can reach into ConSlot::PendingOps directly (see connection.h) -- the same field a // background offload task (e.g. libhttppp's H2 dispatch pool) increments/decrements around // its own work, and that reapIdleConnections() now checks before closing a connection purely // by lasteventime age. class PendingOpsReaperServer : public event { public: std::atomic<con*> lastConnected{nullptr}; PendingOpsReaperServer(std::vector<netplus::socket*> serversockets, int timeout, int idleTimeoutSeconds) : event(serversockets, timeout, idleTimeoutSeconds) { } void RequestEvent(con &curcon, const int tid, ULONG_PTR args) override { static const std::string body = "still-alive\n"; curcon.SendData.append(body.data(), body.size()); curcon.RecvData.clear(); } void ResponseEvent(con &curcon, const int tid, ULONG_PTR args) override {} void ConnectEvent(con &curcon, const int tid, ULONG_PTR args) override { lastConnected = &curcon; } void DisconnectEvent(con &curcon, const int tid, ULONG_PTR args) override {} void CreateConnection(std::shared_ptr<con> &res) override { res = std::make_shared<con>(this); } }; // Runs `srv`'s event loop on a background thread until `body` returns, // then stops it and joins. Centralizes the event::Running dance (a static // flag shared by every netplus::event instance in the process, so the two // flag shared by every netplus::event instance in the process, so the // scenarios below must run strictly sequentially, never concurrently). template <typename Fn> bool withRunningServer(IdleReaperServer &srv, Fn body) { template <typename ServerT, typename Fn> bool withRunningServer(ServerT &srv, Fn body) { event::Running = true; std::thread serverThread([&srv]() { Loading Loading @@ -232,6 +264,97 @@ bool testDefaultDisabledLeavesConnectionAlive() { }); } // Scenario 3: a connection with ConSlot::PendingOps > 0 must NOT be reaped no matter how long // lasteventime has aged -- this is the guard added after a real proxyplus incident where the // reaper closed a connection out from under a still-running background dispatch task (which // doesn't refresh lasteventime), sending a local FIN while the peer was still legitimately // waiting on a response and producing sockets stuck in FIN_WAIT_2 under load. Also proves the // guard is transient, not a permanent opt-out: once PendingOps returns to 0, the very next // sweep reaps the connection exactly like scenario 1. bool testPendingOpsBlocksReap() { std::cout << "[idle_reaper_test] scenario 3: PendingOps > 0 blocks the idle reaper from " "closing a connection despite lasteventime being expired" << std::endl; const int idleTimeoutSeconds = 2; tcp serverSocket("127.0.0.1", 18087, 1024, -1); PendingOpsReaperServer srv({&serverSocket}, /*timeout(ms)=*/200, idleTimeoutSeconds); return withRunningServer(srv, [&]() -> bool { tcp client; client.connect("127.0.0.1", 18087); client.setTimeout(5000); con *serverCon = nullptr; for (int i = 0; i < 50 && !serverCon; ++i) { serverCon = srv.lastConnected.load(); if (!serverCon) std::this_thread::sleep_for(std::chrono::milliseconds(100)); } if (!serverCon) { std::cerr << " FAIL: ConnectEvent never fired" << std::endl; client.close(); return false; } // Simulate a background offload task still in flight on this connection -- exactly // ConSlot::PendingOps's intended use (see connection.h). serverCon->slots[0].PendingOps.fetch_add(1); // Wait past idleTimeoutSeconds + the (overridden) 1s sweep throttle, same margin as // scenario 1 -- long enough that, without the PendingOps guard, this connection would // already have been reaped. std::this_thread::sleep_for(std::chrono::seconds(idleTimeoutSeconds + 4)); bool stillAlive = false; try { const std::string request = "ping"; buffer sendbuf(request.data(), request.size()); client.sendData(sendbuf); buffer recvbuf(64); size_t n = client.recvData(recvbuf); std::string response(recvbuf.data.buf, n); stillAlive = response.find("still-alive") != std::string::npos; if (!stillAlive) std::cerr << " FAIL: unexpected response while PendingOps > 0: " << response << std::endl; } catch (NetException &e) { std::cerr << " FAIL: connection was closed despite PendingOps > 0: " << e.what() << std::endl; } if (!stillAlive) { client.close(); return false; } std::cout << " OK: connection survived the idle sweep while PendingOps > 0" << std::endl; // Release the guard and prove it's transient: the request/response above just // refreshed lasteventime, so wait a full new idle window from here before expecting // the next sweep to reap it now that PendingOps is back to 0. serverCon->slots[0].PendingOps.fetch_sub(1); std::this_thread::sleep_for(std::chrono::seconds(idleTimeoutSeconds + 4)); buffer recvbuf2(64); try { size_t n = client.recvData(recvbuf2); std::cerr << " FAIL: expected the server to reap this connection once PendingOps " "returned to 0, but recvData returned " << n << " bytes instead of EOF" << std::endl; client.close(); return false; } catch (NetException &e) { client.close(); if (e.getErrorType() == NetException::Error) { std::cout << " OK: connection reaped on the next sweep once PendingOps " "returned to 0" << std::endl; return true; } std::cerr << " FAIL: recvData threw, but not a peer-closed error: " << e.what() << std::endl; return false; } }); } } // namespace int main() { Loading