adafruit / circuitpython

CircuitPython - a Python implementation for teaching coding with microcontrollers
https://circuitpython.org
Other
4.04k stars 1.2k forks source link

TCP Connection Timeout Setting #8266

Closed SanJoseBart closed 4 months ago

SanJoseBart commented 1 year ago

In standard Python, socket.settimeout() sets a timeout for both TCP connections and data transfer. In CircuitPython, socket.settimeout() sets a timeout for data transfer, but doesn't affect the TCP connection timeout. It sure would be nice to set a TCP connection timeout, either via settimeout() or some other method.

For example, using CircuitPython 8.2.2 on an Adafruit Feather ESP32-S3 (no PSRAM), an attempted TCP connection always times out after 30 seconds, regardless of the settimeout() value. See below.

# Connection timeout demo

import board
import adafruit_sht31d
import wifi
import time
from socketpool import SocketPool

SSID = "[Wi-Fi name]"
PWD = "[Wi-Fi password]"
SERVER = "10.0.0.119"  # any non-existent local address
PORT = 4790  # any arbitrary port

wifi.radio.connect(SSID, PWD, timeout=10)
starttime = time.monotonic()
try:
    pool = SocketPool(wifi.radio)
    with pool.socket(SocketPool.AF_INET, SocketPool.SOCK_STREAM) as s:
        s.settimeout(5)  # Does not affect connection timeout
        s.connect((SERVER, PORT))
        s.sendall(b"Data goes here")
except OSError as ex:
    elapsed = time.monotonic() - starttime
    print(f"Connection timeout in {elapsed} seconds.")

The example code output is "Connection timeout in 30.5 seconds." (give or take a couple of tenths), regardless of the settimeout() value.

anecdata commented 1 year ago

Looks like it was intended, but as yet unimplemented: https://github.com/adafruit/circuitpython/blob/6b36bf59e30a4d3d93ec9df1f92ae8adc5fe2094/ports/espressif/common-hal/socketpool/Socket.c#L540

birmitt commented 5 months ago

Be able to set the connection timeout is crucial in asyncio environments.

That's actually a bummer as I'm porting the MiniMQTT lib to asyncio to get a fully non-blocking experience. So at least for the connection phase it won't be that "non-blocking" for now.

In my case I would set the connection timeout very small and build a asyncio based retry mechanism on top. That would give other tasks time to run during connection failures.

dhalbert commented 5 months ago

For reference:

https://docs.python.org/3/library/socket.html#socket.socket.settimeout

socket.settimeout(value) Set a timeout on blocking socket operations. The value argument can be a nonnegative floating point number expressing seconds, or None. If a non-zero value is given, subsequent socket operations will raise a timeout exception if the timeout period value has elapsed before the operation has completed. If zero is given, the socket is put in non-blocking mode. If None is given, the socket is put in blocking mode.

For further information, please consult the notes on socket timeouts.

Changed in version 3.7: The method no longer toggles SOCK_NONBLOCK flag on socket.type.

The link to notes on socket timeouts says:

Notes on socket timeouts

A socket object can be in one of three modes: blocking, non-blocking, or timeout. Sockets are by default always created in blocking mode, but this can be changed by calling setdefaulttimeout().

In blocking mode, operations block until complete or the system returns an error (such as connection timed out).

In non-blocking mode, operations fail (with an error that is unfortunately system-dependent) if they cannot be completed immediately: functions from the select module can be used to know when and whether a socket is available for reading or writing.

In timeout mode, operations fail if they cannot be completed within the timeout specified for the socket (they raise a timeout exception) or if the system returns an error.

Note: At the operating system level, sockets in timeout mode are internally set in non-blocking mode. Also, the blocking and timeout modes are shared between file descriptors and socket objects that refer to the same network endpoint. This implementation detail can have visible consequences if e.g. you decide to use the fileno() of a socket. Timeouts and the connect method The connect() operation is also subject to the timeout setting, and in general it is recommended to call settimeout() before calling connect() or pass a timeout parameter to create_connection(). However, the system network stack may also return a connection timeout error of its own regardless of any Python socket timeout setting.

Timeouts and the accept method

If getdefaulttimeout() is not None, sockets returned by the accept() method inherit that timeout. Otherwise, the behaviour depends on settings of the listening socket:

if the listening socket is in blocking mode or in timeout mode, the socket returned by accept() is in blocking mode;

if the listening socket is in non-blocking mode, whether the socket returned by accept() is in blocking or non-blocking mode is operating system-dependent. If you want to ensure cross-platform behaviour, it is recommended you manually override this setting.

dhalbert commented 5 months ago

I did some research on this:

lwip_connect() does not implement SO_CONTIMEO, and thus always uses the default 30-second timeout. This is a well-known issue with lwip. Our code actually sets the socket to be blocking temporarily to do connect(), and thus gets the default, unchangeable, 30-second timeout.

There are some code samples that make the socket be non-blocking and do their own timeout handling: https://github.com/micropython/micropython/pull/12923/files https://github.com/espressif/esp-idf/commit/0c7204e934b8abda433026f9ec9800c5380a657d (tcp_transport is some kind of under-utilized ESP-IDF thingie)