Skip to content
This repository was archived by the owner on Jan 20, 2025. It is now read-only.
This repository was archived by the owner on Jan 20, 2025. It is now read-only.

Possible race condition causing random disconnects #116

Description

@mmakaay

The issue

I am currently working on an ESPHome firmware for a single-core ESP32 MCU. The ESPHome code sets up a server using AsyncTCP. The clients send a ping message every few seconds. Very regularly, the server-side console logs "ack timeout 4" after sending a ping response, disconnecting the connected client. I suspect that this is because of a race condition in AsyncTCP.

Network issue?

The network quality is not the issue here. After disconnecting I always see the Home Assistant client reconnect successfully right away, the network tcp dumps look okay and I can ping the ESP32 continuously. I find various threads on the internet suggesting to disable power management, using a fixed IP address and using wifi fast connect. Those suggestions did not improve my issue.

Log showing the behavior

To debug the issue, I added some extra log messages to AsyncTCP.cpp and here's a log flow that shows the behavior that I see before a disconnect. I added my comments in capitals:

1) A PING REQUEST IS RECEIVED
[17:20:40][VV][api.service:220]: on_ping_request: PingRequest {}

2) THE ESP32 SENDS A PING RESPONSE
[17:20:40][VV][api.service:032]: send_ping_response: PingResponse {}

3) THE METHOD AsyncClient::send() IS CALLED TO SEND THE RESPONSE
[17:20:40]send() send enqueued data with tcp_output(), busy = 0, now = 2119326: 0x3ffd3440

4) THE ACK FOR THE SENT RESPONSE IS RECEIVED AND PROCESSED
BY THE AsyncClient::_sent() CALLBACK (_pcb_busy is set to false)
[17:20:40]-S: 0x3ffd3440
[17:20:40]_sent() sets busy = false, now = 2119336: 0x3ffd3440
[17:20:40]_sent() end of function, busy = 0: 0x3ffd3440

5) WE'RE BACK IN THE AsyncClient::send() METHOD, WHICH NOW SETS _pcb_busy to true
[17:20:40]send() OK sets busy = 1, _pcb_sent_at=2119343, now = 2119343: 0x3ffd3440

6) THE POLLING LOOP IS STARTED, WITH THE INCORRECT _pcb_busy == true STATE
[17:20:40]-P: 0x3ffd3440
[17:20:40][W][AsyncTCP.cpp:965] _poll(): _pcb_busy=1, now=2119394, _pcb_sent_at=2119343, _rx_last_packet=2119336: 0x3ffd3440
[17:20:40]-P: 0x3ffd3440
[17:20:40][W][AsyncTCP.cpp:965] _poll(): _pcb_busy=1, now=2119894, _pcb_sent_at=2119343, _rx_last_packet=2119336: 0x3ffd3440
--------------------8<---------------------------- snipped the polling loglines at this point
[17:20:45]-P: 0x3ffd3440
[17:20:45][W][AsyncTCP.cpp:965] _poll(): _pcb_busy=1, now=2124394, _pcb_sent_at=2119343, _rx_last_packet=2119336: 0x3ffd3440

7) THE POLL LOOP THINKS THAT WE HAVE A TIMEOUT WAITING FOR THE ACK
[17:20:45]_poll() sets busy = false after ack timeout: 0x3ffd3440
[17:20:45][W][AsyncTCP.cpp:971] _poll(): ack timeout 4

8) AND DISCONNECTS THE CONNECTED CLIENT
[17:20:45][V][api.connection:696]: Error: Disconnecting Home Assistant 2021.3.2 (192.168.100.135)
[17:20:45][D][api:067]: Disconnecting Home Assistant 2021.3.2 (192.168.100.135)

9) A NEW CLIENT CONNECTION COMES IN AND IS ACCEPTED RIGHT AWAY
[17:20:45]A: 0x3ffb8b68 0x3ffd3690
[17:20:45]-R: 0x3ffd3d3c
[17:20:45][VV][api.service:192]: on_hello_request: HelloRequest {
[17:20:45]  client_info: 'Home Assistant 2021.3.2'

Looking at this flow, I would say that the flow of the AsyncClient::send() method (3) is interrupted by the lwIP thread that handles the incoming ACK does interrupt (4). This causes the _pcb_busy field to be set to false before AsyncClient::send() sets is to true (5).

Another example

Here's a snippet of log output that even better shows the interruption of the AsyncClient::send() flow. It's an image, so I could color the loglines for clarity.

image

What you see is the red text, which gets interrupted mid sentence by the green text, after which the red text continues where it left off. During the green log lines, the _pcb_busy is set to false and only afterwards the _pcb_busy is set to true in the following red log lines.

Possible solution

Maybe the fix could be as simple as moving setting of _pcb_sent_at and _pcb_busy above the _tcp_output() call, and resetting _pcb_busy in case _tcp_output() fails:

bool AsyncClient::send(){
    int8_t err = ERR_OK;
    _pcb_sent_at = millis();
    _pcb_busy = true;
    err = _tcp_output(_pcb, _closed_slot);
    if(err == ERR_OK){
        return true;
    }
    _pcb_busy = false;
    return false;
}

I will impement this in my local copy, to see if things improve.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions