Commit 931e5ce2 authored by jan.koester's avatar jan.koester
Browse files

test

parent 5cadadad
Loading
Loading
Loading
Loading
+126 −3
Original line number Diff line number Diff line
@@ -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>
@@ -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]() {
@@ -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() {