Crafter.Network/tests/ShouldTimeoutSilentHTTP1/main.cpp

109 lines
3.9 KiB
C++
Raw Normal View History

//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// ClientHTTP1 bounds the TCP connect (connectTimeout) as well as every wait
// for a response (timeout). Before, the connect had no bound of its own: a
// host that drops the SYNs held Send() for the kernel's retries, about two
// minutes, whatever `timeout` said — a readiness probe with a ten-second
// budget could hang its caller for two minutes.
//
// Two servers on loopback. One accepts — the kernel completes the handshake
// from its backlog — and never answers: the read timeout. The other has a
// full accept queue, so Linux drops the SYNs of further connects: the connect
// timeout.
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cstdlib>
import Crafter.Network;
import std;
using namespace Crafter;
using namespace std::chrono_literals;
namespace {
int failures = 0;
void Check(bool ok, std::string_view what) {
std::println("{} {}", ok ? "PASS" : "FAIL", what);
if (!ok) ++failures;
}
// A socket listening on 127.0.0.1, on a port the kernel picks.
struct LoopbackListener {
int fd = -1;
std::uint16_t port = 0;
explicit LoopbackListener(int backlog) {
fd = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in address {};
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
bind(fd, reinterpret_cast<sockaddr*>(&address), sizeof(address));
listen(fd, backlog);
socklen_t length = sizeof(address);
getsockname(fd, reinterpret_cast<sockaddr*>(&address), &length);
port = ntohs(address.sin_port);
}
~LoopbackListener() { close(fd); }
};
long long Milliseconds(std::chrono::steady_clock::duration duration) {
return std::chrono::duration_cast<std::chrono::milliseconds>(duration).count();
}
} // namespace
int main() {
{
LoopbackListener silent(8);
ClientHTTP1 client("127.0.0.1", silent.port);
client.timeout = 300ms;
const auto start = std::chrono::steady_clock::now();
bool failed = false;
try {
client.Send(CreateRequestHTTP("GET", "/", "127.0.0.1"));
} catch (const std::exception&) {
failed = true;
}
const auto waited = std::chrono::steady_clock::now() - start;
Check(failed, "a server that accepts and never answers fails the request");
Check(waited >= 280ms && waited < 5s, std::format("after about `timeout` ({} ms)", Milliseconds(waited)));
}
{
// Connects queue up until the accept queue is full, which with a
// backlog of 0 is after one or two; the next one goes unanswered.
LoopbackListener full(0);
std::vector<ClientTCP> queued;
bool queueFull = false;
for (int i = 0; i < 8 && !queueFull; ++i) {
try {
queued.emplace_back("127.0.0.1", full.port, 200ms);
} catch (const SocketTimeoutException&) {
queueFull = true;
}
}
Check(queueFull, std::format("the accept queue fills up (after {} connects)", queued.size()));
ClientHTTP1 client("127.0.0.1", full.port);
client.connectTimeout = 300ms;
const auto start = std::chrono::steady_clock::now();
bool timedOut = false;
try {
client.Send(CreateRequestHTTP("GET", "/", "127.0.0.1"));
} catch (const SocketTimeoutException&) {
timedOut = true;
} catch (const std::exception&) {
}
const auto waited = std::chrono::steady_clock::now() - start;
Check(timedOut, "a connect nobody answers throws SocketTimeoutException");
Check(waited >= 280ms && waited < 5s, std::format("after about `connectTimeout` ({} ms)", Milliseconds(waited)));
}
std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed");
return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
}