[socket] Fix multi-second TCP stalls on ESP8266 by yielding to the SYS context (#18455)

This commit is contained in:
J. Nick Koston
2026-08-18 13:09:29 +12:00
committed by GitHub
parent 15a626bcf3
commit 4416aacebb
+30 -10
View File
@@ -45,6 +45,11 @@ namespace esphome::socket {
static const char *const TAG = "socket"; static const char *const TAG = "socket";
#ifdef USE_ESP8266
// optimistic_yield() rate limit in microseconds of CONT time; cheap when hot.
static constexpr uint32_t ESP8266_YIELD_INTERVAL_US = 1000;
#endif
// set to 1 to enable verbose lwip logging // set to 1 to enable verbose lwip logging
#if 0 // NOLINT(readability-avoid-unconditional-preprocessor-if) #if 0 // NOLINT(readability-avoid-unconditional-preprocessor-if)
#define LWIP_LOG(msg, ...) ESP_LOGVV(TAG, "socket %p: " msg, this, ##__VA_ARGS__) #define LWIP_LOG(msg, ...) ESP_LOGVV(TAG, "socket %p: " msg, this, ##__VA_ARGS__)
@@ -535,6 +540,14 @@ ssize_t LWIPRawImpl::read_locked_(void *buf, size_t len) {
} }
ssize_t LWIPRawImpl::read(void *buf, size_t len) { ssize_t LWIPRawImpl::read(void *buf, size_t len) {
#ifdef USE_ESP8266
// Would block: yield to SYS so queued WiFi RX reaches lwip and this read
// may succeed. Without this, inbound segments can sit unprocessed for
// seconds while the main loop polls (CONT/SYS are cooperative on ESP8266).
if (this->waiting_for_data_()) {
optimistic_yield(ESP8266_YIELD_INTERVAL_US);
}
#endif
// See waiting_for_data_() for safety of unlocked reads. // See waiting_for_data_() for safety of unlocked reads.
if (this->recv_timeout_cs_ > 0 && this->waiting_for_data_()) { if (this->recv_timeout_cs_ > 0 && this->waiting_for_data_()) {
this->wait_for_data_(); this->wait_for_data_();
@@ -545,6 +558,8 @@ ssize_t LWIPRawImpl::read(void *buf, size_t len) {
} }
ssize_t LWIPRawImpl::readv(const struct iovec *iov, int iovcnt) { ssize_t LWIPRawImpl::readv(const struct iovec *iov, int iovcnt) {
// No ESP8266 SYS yield here: only read() needs it today. If a consumer
// switches to scatter-gather reads, mirror the yield from read().
// See waiting_for_data_() for safety of unlocked reads. // See waiting_for_data_() for safety of unlocked reads.
if (this->recv_timeout_cs_ > 0 && this->waiting_for_data_()) { if (this->recv_timeout_cs_ > 0 && this->waiting_for_data_()) {
this->wait_for_data_(); this->wait_for_data_();
@@ -609,19 +624,24 @@ int LWIPRawImpl::internal_output_() {
} }
LWIP_LOG("tcp_output(%p)", this->pcb_); LWIP_LOG("tcp_output(%p)", this->pcb_);
err_t err = tcp_output(this->pcb_); err_t err = tcp_output(this->pcb_);
if (err == ERR_ABRT) {
// sometimes lwip returns ERR_ABRT for no apparent reason
// the connection works fine afterwards, and back with ESPAsyncTCP we
// indirectly also ignored this error
// FIXME: figure out where this is returned and what it means in this context
LWIP_LOG(" -> err ERR_ABRT");
return 0;
}
if (err != ERR_OK) { if (err != ERR_OK) {
LWIP_LOG(" -> err %d", err); LWIP_LOG(" -> err %d", err);
errno = ECONNRESET; // ERR_ABRT: sometimes lwip returns it for no apparent reason; the
return -1; // connection works fine afterwards, and back with ESPAsyncTCP we
// indirectly also ignored this error, so treat it as success for
// flush purposes too.
// FIXME: figure out where this is returned and what it means in this context
if (err != ERR_ABRT) {
errno = ECONNRESET;
return -1;
}
} }
#ifdef USE_ESP8266
// Flushed: yield to SYS so the queued segments reach the WiFi driver
// instead of waiting seconds for an unrelated SYS slot. Callers only get
// here after a successful tcp_write, so idle paths never yield.
optimistic_yield(ESP8266_YIELD_INTERVAL_US);
#endif
return 0; return 0;
} }