aio-libs / aiozmq

Asyncio (pep 3156) integration with ZeroMQ
aiozmq.readthedocs.org
BSD 2-Clause "Simplified" License
422 stars 56 forks source link

stream.write() prevents Python process from terminating after loop clean up using task cancellation #61

Closed achimnol closed 9 years ago

achimnol commented 9 years ago

write() calls to an aiozmq stream prevent "clean termination" of the Python process. Here is a minimal working example:

#! /usr/bin/env python3
import asyncio, aiozmq, zmq

@asyncio.coroutine
def test(loop):
    # connect to a non-existent endpoint (this returns immediately)
    sock = yield from aiozmq.create_zmq_stream(zmq.REQ, connect='tcp://localhost:9999', loop=loop)
    sock.write([b'test'])
    # just make some time to press Ctrl+C
    yield from asyncio.sleep(5)
    # below is not executed as sleep is cancelled.
    _ = yield from sock.read()
    sock.close()

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    asyncio.async(test(loop))
    try: 
        loop.run_forever()
    except KeyboardInterrupt:
        for t in asyncio.Task.all_tasks():
            if not t.done():
                t.cancel()  # cancels the sleep call
        loop.run_until_complete(asyncio.sleep(0))
    finally:
        loop.close()
        print('exit')

The result when you press Ctrl+C during sleep is:

$ python3 test-aiozmq-write-cancel.py
^Cexit   # Python hangs here; If I comment the write() call, it goes normal.
^C       # I need to press Ctrl+C again to terminate the Python process completely.

Is there any way to correctly interrupt/cancel the background behavior of write() calls?

achimnol commented 9 years ago

Using gdb, I found what is blocking after my code has finished.

(gdb) bt
#0  0x00007ffff71d712d in poll () at ../sysdeps/unix/syscall-template.S:81
#1  0x00007ffff3700a6a in ?? () from /usr/lib/x86_64-linux-gnu/libzmq.so.3
#2  0x00007ffff36ed2f7 in ?? () from /usr/lib/x86_64-linux-gnu/libzmq.so.3
#3  0x00007ffff36e463c in ?? () from /usr/lib/x86_64-linux-gnu/libzmq.so.3
#4  0x00007ffff3715388 in zmq_ctx_term () from /usr/lib/x86_64-linux-gnu/libzmq.so.3
#5  0x00007ffff294c7e9 in __pyx_f_3zmq_7backend_6cython_7context_7Context__term (__pyx_v_self=0x7ffff24b5f28) at zmq/backend/cython/context.c:1773
#6  __pyx_pf_3zmq_7backend_6cython_7context_7Context_4__dealloc__ (__pyx_v_self=0x7ffff24b5f28) at zmq/backend/cython/context.c:1391
#7  __pyx_pw_3zmq_7backend_6cython_7context_7Context_5__dealloc__ (__pyx_v_self=0x7ffff24b5f28) at zmq/backend/cython/context.c:1323
#8  __pyx_tp_dealloc_3zmq_7backend_6cython_7context_Context (o=0x7ffff24b5f28) at zmq/backend/cython/context.c:3093
#9  0x0000000000487176 in subtype_dealloc (self=0x7ffff24b5f28) at Objects/typeobject.c:1226
#10 0x000000000046914f in free_keys_object (keys=0xc0f5f0) at Objects/dictobject.c:354
#11 PyDict_Clear (op=<optimized out>) at Objects/dictobject.c:1323
#12 0x000000000046b532 in PyDict_Clear (op=<optimized out>) at Objects/dictobject.c:1301
#13 0x000000000048b9d8 in type_clear (type=0xc0b638) at Objects/typeobject.c:3282
#14 0x0000000000542992 in delete_garbage (old=<optimized out>, collectable=<optimized out>) at Modules/gcmodule.c:866
#15 collect (generation=generation@entry=2, n_collected=n_collected@entry=0x0, n_uncollectable=n_uncollectable@entry=0x0, nofail=nofail@entry=1) at Modules/gcmodule.c:1014
#16 0x00000000005435b1 in _PyGC_CollectNoFail () at Modules/gcmodule.c:1605
#17 0x000000000050e33c in PyImport_Cleanup () at Python/import.c:481
#18 0x0000000000521de9 in Py_Finalize () at Python/pylifecycle.c:576
#19 0x0000000000521fb5 in Py_Finalize () at Python/pylifecycle.c:524
#20 0x00000000004204d7 in Py_Main (argc=argc@entry=2, argv=argv@entry=0x8ee010) at Modules/main.c:788
#21 0x000000000041c3b7 in main (argc=2, argv=<optimized out>) at ./Programs/python.c:69
asvetlov commented 9 years ago

Hmm. Looks interesting. Do you have hang on zmq context termination stage? Not sure what can I change in aiozmq but you may try zmq.Context.instance().term() after loop.close() call.

achimnol commented 9 years ago

Oh, I just googled a bit and experimented another bit, and found that setting LINGER option resolves this problem. e.g., Insertsock.transport.setsockopt(zmq.LINGER, 50) just after the create_zmq_stream() call.

It seems that the underlying TCP connection was not established (because I'm using a wrong address intentionally) and the followed write call makes the termination process of libzmq to poll availability of the TCP connection internally.

achimnol commented 9 years ago

Therefore, this is not the "fault" of aiozmq at all; I'm closing the issue. :)