Fix TLS write busy-spin on SSL_ERROR_WANT_READ

A TLS sendData() failing with SSL_ERROR_WANT_READ (mid-handshake
application data, or a post-handshake session ticket exchange) has
nothing to do with write-readiness, but resetSocket() kept re-arming
EPOLLWRNORM anyway - an already-writable TCP socket fires that
immediately, so the retry loop spun the CPU as fast as it could
until the connection was eventually dropped, without ever reading
the peer's first command. Confirmed via a 15GB+ debug log from one
stuck connection. sendNeedsRead() lets Socket now tell the
difference and wait for a real EPOLLIN instead of busy-retrying.
This commit is contained in:
Brad Arant committed 2026-09-06 04:05:40 +00:00
1 parent c3e8d70774
commit 2676aaf84c
6 files changed
+68 -1

No files matched your search

+24
View File
@@ -182,6 +182,17 @@ namespace core {
virtual int sendData(const char *data, int length);
///
/// Overridden by TCPSocket to report whether the last sendData()
/// call failed specifically because the TLS layer needs a READ to
/// make progress (SSL_ERROR_WANT_READ), as opposed to any other
/// reason - see waitingOnReadToWrite's own doc comment for why
/// writeSocket()/resetSocket() need to know this. Always false for
/// a plain (non-TLS) socket, which has no such distinction.
///
virtual bool sendNeedsRead() { return false; }
private:
std::string text;
@@ -226,6 +237,19 @@ namespace core {
// clearly never going to un-stick itself and gets shut down.
int consecutiveWriteStalls = 0;
// Set by writeSocket() when the last failed send specifically
// needed a read to proceed (sendNeedsRead() true) rather than
// being genuinely stalled on write-readiness. resetSocket() skips
// adding EPOLLWRNORM to the re-armed event mask while this is
// true - re-arming it anyway just spins the CPU as fast as it can
// loop, since an already-writable TCP socket keeps firing
// EPOLLWRNORM immediately while the actual blocker (more bytes
// from the peer) has nothing to do with write-readiness at all.
// Cleared on any EPOLLIN event (see eventReceived()) so the write
// gets a fresh attempt via that same event's own trailing
// resetSocket() call, once there's real reason to expect progress.
bool waitingOnReadToWrite = false;
void resetSocket();
std::mutex lock;