Skip to content

uselect Poll.ipoll() returns unexpected events with 'userdata' feature #46

Open
@AndyFug

Description

@AndyFug

Hi,
Novice to this world, but hoping that this is a valid contribution. Apologies if I've missed something obvious or if this report isn't clear enough/in the correct place. Sorry that it's a little lengthy but I'm led to believe that more (useful) information is often better than less. Feedback accepted. :)

I was trying to use Picoweb/Uasyncio with Pycopy on an ESP8266, when I stumbled across the EAGAIN issues discussed here:
#30
micropython/micropython#4824

I applied the work-around suggested by catching the exception on s.accept() in uasyncio start_server() loop, but that then unveiled what appeared to be going on (once you enabled verbose logging). It appeared to me that uselect Poll.ipoll() method was returning unexpected events, so when uasyncio's start_server() loop was expecting a POLLIN event after yielding IORead(s) it was, in fact, receiving other events - mostly POLLOUT(4), hence why it was expecting to accept new client connections when there weren't any and raising the EAGAIN exception.

To look into this further, I came up with a minimal test-case for uselect using the same methods as uasyncio, based on others' code from previous posts:

import usocket as socket, uselect as select, ulogging as logging, gc

_blocking = False
_userdata = True
_max_events = 1000
_start_mem = 0

log = logging.getLogger("test")


def issock(s1, s2): return s1 == s2


# Two options for register method.  With userdata and without.
def register(poller, s):
    if _userdata:
        poller.register(s, select.POLLIN, "Python object")
    else:
        poller.register(s, select.POLLIN)


def start_server(port):
    ai = socket.getaddrinfo('0.0.0.0', port)[0][-1]
    print("Starting server... Port: {}".format(port))
    print("Bug in place: {}".format(_userdata))
    ss = socket.socket()
    ss.bind(ai)
    ss.listen(10)
    ss.setblocking(_blocking)
    ss.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    poll = select.poll()
    register(poll, ss)

    i = 0

    gc.collect()
    start_mem = gc.mem_free()

    while True:
        gc.collect()
        log.debug("Memory used: {}".format(start_mem - gc.mem_free()))

        # One-shot behaviour
        res = poll.ipoll(-1, 1)
        for s, e, obj in res:
            i += 1
            log.debug("#{0} NEW SOCKET EVENT: Socket: {1}  Event: {2}  Userdata: {3}".format(i, s, e, obj))

            # New client connection on server socket
            if issock(s, ss) and e == select.POLLIN:
                s, addr = ss.accept()
                s.setblocking(_blocking)
                del addr
                # Imitate uasyncio... use register() not modify()
                register(poll, s)
                register(poll, ss)
                log.debug("New connection!")
                continue

            # Assume new client message
            if not issock(s, ss) and e == select.POLLIN:
                log.debug("Trying to read from socket...")
                msg = s.recv(1024)
                if msg:
                    print("*** NEW MESSAGE: {}".format(msg))
                    register(poll, s)

                # Empty msg > close connection
                if not msg:
                    log.debug("Closing socket...")
                    poll.unregister(s)
                    s.close()

            # If the event is not POLLIN, just log it.
            else:
                log.debug("Unhandled event: {} on socket: {}".format(e, s))
                # Re-register it for POLLIN becuase otherwise we won't get any future events (single-shot mode)
                register(poll, s)

        # Stop the server after x events
        if i >= _max_events:
            print("Closing down server...")
            poll.unregister(ss)
            ss.close()
            break


def run(userdata=True, debug=True, port=8000):
    global _userdata
    _userdata = userdata

    if debug is True:
        log.setLevel(logging.DEBUG)
    else:
        log.setLevel(logging.INFO)

    start_server(port)

After playing around with this code on the ESP8266, I discovered that the issue described only appeared to come about when adding the 'userdata' argument to the Poll.register() method. The code above can be run with/without 'userdata' and, for me, it only works as expected when the 'userdata' feature isn't used. I used a basic packet sender application as a client to send short ASCII strings. Here are the outputs:

With Poll.register() 'userdata' argument:
(No client connections during this output... just the server running on its own)

Pycopy v1.11-1462-gaf0948daa on 2020-06-14; ESP module with ESP8266
Type "help()" for more information.
>>> uatest1.run(userdata=True)
Starting server... Port: 8000
Bug in place: True
DEBUG:test:Memory used: 0
DEBUG:test:#1 NEW SOCKET EVENT: Socket: <socket state=1 timeout=0 incoming=a off=0>  Event: 4  Userdata: Python object
DEBUG:test:Unhandled event: 4 on socket: <socket state=1 timeout=0 incoming=a off=0>
DEBUG:test:Memory used: 32
DEBUG:test:#2 NEW SOCKET EVENT: Socket: <socket state=1 timeout=0 incoming=a off=0>  Event: 4  Userdata: Python object
DEBUG:test:Unhandled event: 4 on socket: <socket state=1 timeout=0 incoming=a off=0>
DEBUG:test:Memory used: 32
DEBUG:test:#3 NEW SOCKET EVENT: Socket: <socket state=1 timeout=0 incoming=a off=0>  Event: 4  Userdata: Python object
DEBUG:test:Unhandled event: 4 on socket: <socket state=1 timeout=0 incoming=a off=0>
DEBUG:test:Memory used: 32
DEBUG:test:#4 NEW SOCKET EVENT: Socket: <socket state=1 timeout=0 incoming=a off=0>  Event: 4  Userdata: Python object
DEBUG:test:Unhandled event: 4 on socket: <socket state=1 timeout=0 incoming=a off=0>
DEBUG:test:Memory used: 32

Without Poll.register() 'userdata' argument:
(+ Client connection with 3 ASCII strings sent client > server)

Pycopy v1.11-1462-gaf0948daa on 2020-06-14; ESP module with ESP8266
Type "help()" for more information.
>>> uatest1.run(userdata=False)
Starting server... Port: 8000
Bug in place: False
DEBUG:test:Memory used: 0
DEBUG:test:#1 NEW SOCKET EVENT: Socket: <socket state=1 timeout=0 incoming=1000a off=0>  Event: 1  Userdata: None
DEBUG:test:New connection!
DEBUG:test:Memory used: 96
DEBUG:test:#2 NEW SOCKET EVENT: Socket: <socket state=3 timeout=0 incoming=3fff9110 off=0>  Event: 1  Userdata: None
DEBUG:test:Trying to read from socket...
*** NEW MESSAGE: b'Testing - Message 1'
DEBUG:test:Memory used: 144
DEBUG:test:#3 NEW SOCKET EVENT: Socket: <socket state=3 timeout=0 incoming=3fff9110 off=0>  Event: 1  Userdata: None
DEBUG:test:Trying to read from socket...
*** NEW MESSAGE: b'Message 2\r'
DEBUG:test:Memory used: 128
DEBUG:test:#4 NEW SOCKET EVENT: Socket: <socket state=3 timeout=0 incoming=3fff9110 off=0>  Event: 1  Userdata: None
DEBUG:test:Trying to read from socket...
*** NEW MESSAGE: b'Message 3 - Closing after this.\r'
DEBUG:test:Memory used: 160
DEBUG:test:#5 NEW SOCKET EVENT: Socket: <socket state=4 timeout=0 incoming=0 off=0>  Event: 1  Userdata: None
DEBUG:test:Trying to read from socket...
DEBUG:test:Closing socket...
DEBUG:test:Memory used: 96

Extra info:

  • Pycopy version: v1.11-1462-gaf0948daa
  • Unix port: Works correctly on unix port with/without userdata argument. No issues.
  • Client Connections: When running with userdata arguments, ipoll returns event '5' when a client attempts to connect. Can't find out what event 5 means or if it's just a mistake?

For my Picoweb app (which was leaking memory after consecutive client connections), I've reverted uasyncio's application of userdata for callbacks by rolling back commit: pfalcon/pycopy-lib@69fecaf. This seems to have fixed my memory leak issue for now.

Sadly I'm not experienced enough to find the root cause of the issue myself. I suspect it could lie in a C module (Such as this one?) or lwIP stack, but I don't have the experience to follow what's going on at this low level.

Many thanks in advance to anyone who looks into this with me, and for all the fantastic work you guys have done on these projects.

Kind regards,

Andy

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