KeyError en el módulo ‘threading’ después de una ejecución exitosa de py.test

Estoy ejecutando un conjunto de pruebas con py.test. Pasan. Yippie! Pero estoy recibiendo este mensaje:

Exception KeyError: KeyError(4427427920,) in  ignored 

¿Cómo debo hacer para localizar la fuente de eso? (No estoy usando hilos directamente, pero estoy usando gevent.)

Observé un problema similar y decidí ver qué sucede exactamente, permítame describir mis hallazgos. Espero que alguien lo encuentre útil.

Cuento

De hecho, está relacionado con parchear el módulo de threading . De hecho, puedo activar fácilmente la excepción importando el módulo de subprocesamiento antes de los subprocesos de parches de mono. Las siguientes 2 líneas son suficientes:

 import threading import gevent.monkey; gevent.monkey.patch_thread() 

Cuando se ejecuta, escupe el mensaje sobre KeyError ignorado:

 (env)czajnik@autosan:~$ python test.py Exception KeyError: KeyError(139924387112272,) in  ignored 

Si intercambias las líneas de importación, el problema desaparece.

Larga historia

Podría detener mi depuración aquí, pero decidí que vale la pena entender la causa exacta del problema.

El primer paso fue encontrar el código que imprime el mensaje sobre la excepción ignorada. Fue un poco difícil para mí encontrarlo (grepping for Exception.*ignored Ignorado Exception.*ignored produjo nada), pero grepping alrededor del código fuente de CPython finalmente encontré una función llamada void PyErr_WriteUnraisable(PyObject *obj) en Python / error.c , con un comentario muy interesante:

 /* Call when an exception has occurred but there is no way for Python to handle it. Examples: exception in __del__ or during GC. */ 

Decidí verificar quién lo está llamando, con un poco de ayuda de gdb , solo para obtener el siguiente seguimiento de stack de nivel C:

 #0 0x0000000000542c40 in PyErr_WriteUnraisable () #1 0x00000000004af2d3 in Py_Finalize () #2 0x00000000004aa72e in Py_Main () #3 0x00007ffff68e576d in __libc_start_main (main=0x41b980 
, argc=2, ubp_av=0x7fffffffe5f8, init=, fini=, rtld_fini=, stack_end=0x7fffffffe5e8) at libc-start.c:226 #4 0x000000000041b9b1 in _start ()

Ahora podemos ver claramente que se lanza la excepción mientras se ejecuta Py_Finalize ; esta llamada es responsable de apagar el intérprete de Python, liberar la memoria asignada, etc. Se llama justo antes de salir.

El siguiente paso fue ver el código Py_Finalize() (está en Python / pythonrun.c ). La primera llamada que hace es wait_for_thread_shutdown() : vale la pena wait_for_thread_shutdown() , ya que sabemos que el problema está relacionado con el subprocesamiento. Esta función, a su vez, llama a _shutdown en el módulo de threading . Bien, podemos volver al código de Python ahora.

Mirando threading.py he encontrado las siguientes partes interesantes:

 class _MainThread(Thread): def _exitfunc(self): self._Thread__stop() t = _pickSomeNonDaemonThread() if t: if __debug__: self._note("%s: waiting for other threads", self) while t: t.join() t = _pickSomeNonDaemonThread() if __debug__: self._note("%s: exiting", self) self._Thread__delete() # Create the main thread object, # and make it available for the interpreter # (Py_Main) as threading._shutdown. _shutdown = _MainThread()._exitfunc 

Claramente, la responsabilidad de la llamada threading._shutdown() es unir todos los hilos no daemon y eliminar el hilo principal (lo que sea que signifique exactamente). Decidí parchear threading.py un poco: envuelva todo el cuerpo _exitfunc() con try / except e imprima la traza de la stack con el módulo de rastreo. Esto le dio el siguiente rastro:

 Traceback (most recent call last): File "/usr/lib/python2.7/threading.py", line 785, in _exitfunc self._Thread__delete() File "/usr/lib/python2.7/threading.py", line 639, in __delete del _active[_get_ident()] KeyError: 26805584 

Ahora sabemos el lugar exacto donde se produce la excepción: dentro del método Thread.__delete() .

El rest de la historia es evidente después de leer threading.py por un tiempo. El diccionario _active asigna los ID de hilos (como los devuelve _get_ident() ) a las instancias de hilos, para todos los hilos creados. Cuando se carga el módulo de threading , siempre se crea una instancia de la clase _MainThread y se agrega a _active (incluso si no se crea explícitamente ningún otro subproceso).

El problema es que uno de los métodos parcheados por el parche de mono de _get_ident() es _get_ident() – uno original se asigna a thread.get_ident() , el parche de mono lo reemplaza por green_thread.get_ident() . Obviamente ambas llamadas devuelven diferentes identificaciones para el hilo principal.

Ahora, si el módulo de threading se carga antes de la aplicación de parches mono, la llamada _get_ident() devuelve un valor cuando se _MainThread instancia _MainThread y se agrega a _active , y se _active otro valor en el momento _exitfunc() , por KeyError tanto KeyError in del _active[_get_ident()] .

Por el contrario, si el parcheo de mono se realiza antes de que se cargue el threading , todo está bien, en el momento en que se _MainThread instancia de _active a _active , _get_ident() ya está parcheado, y el mismo ID de hilo se devuelve en el momento de la limpieza. ¡Eso es!

Para asegurarme de importar módulos en el orden correcto, agregué el siguiente fragmento de código a mi código, justo antes de la llamada de parches de mono:

 import sys if 'threading' in sys.modules: raise Exception('threading module loaded before patching!') import gevent.monkey; gevent.monkey.patch_thread() 

Espero que encuentre útil mi historia de depuración 🙂

Podrías usar esto:

 import sys if 'threading' in sys.modules: del sys.modules['threading'] import gevent import gevent.socket import gevent.monkey gevent.monkey.patch_all() 

Tuve un problema similar con un script de prototipo gevent.

La callback de Greenlet se estaba ejecutando bien y estaba sincronizando de nuevo con el hilo principal a través de g.join (). Para mi problema, tuve que llamar a gevent.shutdown () para apagar (lo que supongo que es) el Hub. Después de cerrar manualmente el bucle de eventos, el progtwig finaliza correctamente sin ese error.