Skip to content

asyncio TCP socket code hard faults on espressif #10775

@anecdata

Description

@anecdata

CircuitPython version and board name

tested various versions from
Adafruit CircuitPython 9.2.8 on 2025-05-28; Adafruit QT Py ESP32-S3 4MB Flash 2MB PSRAM with ESP32S3
through
Adafruit CircuitPython 10.1.0-beta.1 on 2025-11-06; Adafruit QT Py ESP32-S3 4MB Flash 2MB PSRAM with ESP32S3

Code/REPL

import time
import random
import os
import wifi
import socketpool
import asyncio


NUM_CLIENTS = 4
NUM_SERVERS = 2
BASE_PORT = 5000
MAXBUF = 64


class Socket:
    """ https://github.com/adafruit/circuitpython/pull/7173 """
    def __init__(self, s):
        self.s = s
        s.setblocking(False)

    async def recv_into(self, buf):
        await asyncio.core._io_queue.queue_read(self.s)
        return self.s.recv_into(buf)

    async def send(self, buf):
        await asyncio.core._io_queue.queue_write(self.s)
        return self.s.send(buf)


async def accepted_conn(server_num, conn, event):
    """handles a single server connection from a client"""

    inbuf = bytearray(MAXBUF)

    sconn = Socket(conn)

    size = False
    while not size:
        try:
            size = await(sconn.recv_into(inbuf))  # OSError: [Errno 128] ENOTCONN
        except OSError as ex:
            await asyncio.sleep(0)
    print(f"{time.monotonic():.1f}s SERVER {server_num} received {size} bytes {inbuf[:size]}")

    outbuf = f"{time.monotonic():.1f}s Hello client from server {server_num}".encode()
    await(sconn.send(outbuf))
    print(f"{time.monotonic():.1f}s SERVER {server_num} sent {len(outbuf)} bytes {outbuf}")

    # sconn.close()  # AttributeError: 'Socket' object has no attribute 'close'
    conn.close()
    event.set()

async def server(server_num):
    """persistent TCP server"""

    BACKLOG = NUM_CLIENTS

    s = pool.socket(pool.AF_INET, pool.SOCK_STREAM)
    s.setsockopt(pool.SOL_SOCKET, pool.SO_REUSEADDR, 1)  # 
    port = BASE_PORT + server_num
    s.bind((host, port))
    s.listen(BACKLOG)
    print(f"{time.monotonic():.1f}s SERVER {server_num} listening on port {port}")
    total = 0
    while True:
        print(f"{time.monotonic():.1f}s SERVER {server_num} accepting connections")
        s.setblocking(False)
        addr = False
        while not addr:
            try:
                conn, addr = s.accept()  # OSError: [Errno 11] EAGAIN
                print(f"{time.monotonic():.1f}s SERVER {server_num} accepted connection from {addr}")
                event = asyncio.Event()
                t = asyncio.create_task(accepted_conn(server_num, conn, event))
                await event.wait()
                t.cancel()
                total += 1
                print(f"{time.monotonic():.1f}s SERVER {server_num} has processed {total} total connections")
            except OSError as ex:
                await asyncio.sleep(0)
        await asyncio.sleep(0)

async def connected_client(client_num, s, event):
    """handles a single client connection to a server"""

    inbuf = bytearray(MAXBUF)

    ss = Socket(s)

    outbuf = f"{time.monotonic():.1f}s Hello server from client {client_num}".encode()
    size = await(ss.send(outbuf))
    print(f"{time.monotonic():.1f}s CLIENT {client_num} sent {size} bytes {outbuf}")

    size = False
    while not size:
        try:
            size = await(ss.recv_into(inbuf))  # OSError: [Errno 11] EAGAIN
        except OSError as ex:
            await asyncio.sleep(0)
    print(f"{time.monotonic():.1f}s CLIENT {client_num} received {size} bytes {inbuf[:size]}")

    # ss.close()  # AttributeError: 'Socket' object has no attribute 'close'
    event.set()

async def client(client_num):
    """TCP client wrapper"""

    total = 0
    while True:
        with pool.socket(pool.AF_INET, pool.SOCK_STREAM) as s:
            port = random.randint(BASE_PORT, BASE_PORT + NUM_SERVERS)
            print(f"{time.monotonic():.1f}s CLIENT {client_num} connecting to ({host}, {port})")
            try:
                s.connect((host, port))  # OSError: [Errno 119] EINPROGRESS
                print(f"{time.monotonic():.1f}s CLIENT {client_num} connected to ({host}, {port})")
                event = asyncio.Event()
                t = asyncio.create_task(connected_client(client_num, s, event))
                await event.wait()
                t.cancel
                total += 1
                print(f"{time.monotonic():.1f}s CLIENT {client_num} has processed {total} total connections")
            except OSError as ex:
                await asyncio.sleep(0)
        await asyncio.sleep(random.random())


async def main():
    print(f"{time.monotonic():.1f}s creating {NUM_SERVERS} servers, {NUM_CLIENTS} clients")
    tasks = []
    for server_num in range(NUM_SERVERS):
        tasks.append(asyncio.create_task(server(server_num)))
    for client_num in range(NUM_CLIENTS):
        tasks.append(asyncio.create_task(client(client_num)))
    print(f"{time.monotonic():.1f}s {len(tasks)} tasks created")
    while True:
        await asyncio.sleep(0)


time.sleep(3)  # wait for serial after reset
wifi.radio.connect(os.getenv('WIFI_SSID'), os.getenv('WIFI_PASSWORD'))
host = str(wifi.radio.ipv4_address)
pool = socketpool.SocketPool(wifi.radio)

asyncio.run(main())

Behavior

Espressif: ```Hard fault: memory access or instruction error.````

The time it takes to hard fault varies, but typically 1-8 hours.

Tried variations, including single server and single client, and other simplifications. This code uses TCP. I had less success constructing a UDP version that would run without exceptions.

Code does not run on raspberrypi (various exceptions).

I'm really not sure if async sockets are supposed to be supported at this point. There are numerous asyncio-socket-related issues (and some abandoned PRs) in the core, and in the asyncio and network libraries.

The inspiration for this test came from:
#7173
I'm not sure how canonical this pattern is. It doesn't seem to be CPython compatible, and it may be incomplete for fullly-featured asynchronous sockets.

Initially reported here:
https://gist.github.com/anecdata/89b4458fd7c0c01c7fc0a9ff4be5b4e6

Description

No response

Additional information

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions