Add a stale/never-active session reaper and raise the listen backlog

TCPServer accepted a connection and never revisited it again once it
stopped generating epoll events - a connection that completes the TCP
handshake and then goes permanently silent (an incomplete TLS
handshake, a port scanner/prober) sat in sessions forever, one
TCPSession + one fd each. sweepStaleSessions() now drops any session
that has gone 30+ seconds without a single byte of real data ever
passing through onDataReceived(), run each time a new connection
arrives rather than needing a dedicated timer. hasReceivedData latches
permanently true on a sessions first real byte, so an ordinary
connected-but-briefly-idle client is never at risk.

Also raised listen()s backlog from 20 to 128, so a burst of connection
attempts has more headroom before the kernel starts dropping SYNs.

Verified live on zephora.barant.com: a silent connection held open was
reaped within the grace window while a real concurrent connection was
left untouched, with the service remaining healthy throughout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Matt ArantandClaude Sonnet 5 committed 2026-10-06 17:54:20 +00:00
1 parent d058322cc6
commit 6483729de3
4 files changed
+103 -1

No files matched your search

+41 -1
View File
@@ -3,6 +3,8 @@
#include "Exception.h"
#include "Log.h"
#include "TCPSession.h"
#include <chrono>
#include <vector>
namespace core {
@@ -30,7 +32,14 @@ namespace core {
if (bind(getDescriptor(), address.getPointer(), address.addressLength) < 0)
throw coreutils::Exception("Error on bind to socket: " + std::to_string(errno));
if (listen(getDescriptor(), 20) < 0)
// Was 20 - small enough that a burst of connection attempts
// arriving faster than this process calls accept() (a port scan,
// or just several real clients reconnecting around the same
// moment) can fill the kernel's own backlog before the
// application layer gets a chance to drain it, silently dropping
// the overflow SYNs rather than queuing them. 128 gives real
// bursts meaningfully more headroom; still bounded, not unlimited.
if (listen(getDescriptor(), 128) < 0)
throw coreutils::Exception("Error on listen to socket");
// See isKnownListeningSocket()'s own doc comment - registers this
@@ -49,6 +58,10 @@ namespace core {
void TCPServer::connectionRequest() {
coreutils::Log(coreutils::LOG_DEBUG_2) << "Connection request is being received on socket " << getDescriptor() << ".";
// Outside the lock.lock()/unlock() below, not nested inside it -
// sweepStaleSessions() takes the same `lock` itself (see its own
// comment), and this mutex isn't recursive.
sweepStaleSessions();
lock.lock();
TCPSession *session = accept();
// The shared_ptr constructed here IS the session's real, strong
@@ -80,6 +93,33 @@ namespace core {
lock.unlock();
}
void TCPServer::sweepStaleSessions() {
// 30 seconds is generous for completing a TCP handshake plus a
// TLS handshake plus sending a first real message over any real
// network - deliberately not aimed at catching anything borderline,
// only connections that never progressed at all.
static const std::chrono::seconds GRACE_PERIOD(30);
auto now = std::chrono::steady_clock::now();
// Collect under `lock` (sessions is mutated elsewhere under the
// same mutex - see its own doc comment), then drop the lock before
// actually removing anything. removeFromSessionList() takes `lock`
// itself for each call; calling it while still holding the guard
// below would deadlock on this non-recursive mutex.
std::vector<TCPSession *> stale;
{
std::lock_guard<std::mutex> guard(lock);
for (auto &s : sessions)
if (!s->hasReceivedData && (now - s->acceptedAt) > GRACE_PERIOD)
stale.push_back(s.get());
}
for (auto *s : stale) {
coreutils::Log(coreutils::LOG_DEBUG_2) << "Reaping stale, never-active session " << s->getDescriptor() << ".";
removeFromSessionList(s);
}
}
void TCPServer::receiveData(coreutils::ZString &buffer) {
// See this override's own doc comment in TCPServer.h - the
// listening socket always uses the plain read()/ENOTCONN detection