109 lines
3.9 KiB
C++
109 lines
3.9 KiB
C++
|
|
//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;
|
||
|
|
}
|