La exactitud de Gevent (parte 2 de 4)

Este artículo fue publicado originalmente el 14 de octubre de 2020 en eng.lyft.com por Roy Williams y fue traducido por Fernando A. López P.

Como mencionamos en la parte 1, gevent hace que todo el código mágicamente coopere, pero no todo el código fue escrito para ser cooperativo.

Esto puede llevar a problemas en producción muy sutiles que son increíblemente difíciles de corregir, con código que pasa las pruebas pero se quiebra en condiciones reales. Considera el código de abajo — imaginemos que tenemos una base de datos en memoria de saldos de cuentas y enviamos dinero en un círculo. Al final, no debería haber diferencia en los saldos: todos deberían tener $100.

¡Prueba exitosa!
{'rodolfo': 100, 'juan': 100, 'nacho': 100}

Hasta ahora, todo bien. Tenemos una prueba unitaria que pasa e, incluso tras cien solicitudes concurrentes, todos tienen el saldo correcto. Ahora imaginemos que queremos introducir algo de observabilidad y revisar cuánto dinero se está moviendo. Queremos maximizar la utilización de nuestro servicio así que la biblioteca de estadísticas usa gevent. calcular_nuevos_saldos parece ser el lugar adecuado para hacerlo, de modo que añadimos una llamada a la función de estadísticas. Hacemos el cambio y corremos nuestra prueba unitaria de nuevo para asegurarnos de que dicho cambio es seguro.

¡Prueba exitosa!

¡Perfecto! Introducimos estadísticas, las pruebas pasaron, ¿cuánto peligro puede haber? Corramos a este amigo en producción…

{'rodolfo': 310, 'juan': 310, 'nacho': 110}

Oh, no… todos deberían tener el mismo saldo al final pero en la práctica quedaron muy disparejos. Nuestros clientes van a estar muy decepcionados (¡o muy contentos!). ¿Cómo pasó esto? Habíamos probado que nuestro código en Banco era correcto usando pruebas unitarias. El meollo del asunto está en que el código de Banco había sido escrito asumiendo su ejecución en serie e ininterrumpida — enviar_dinero asumía que la base de datos no sería modificada entre el momento en el que leemos de ella y el momento en el que le escribimos. Esta suposición era correcta antes de que la llamada a la función de estadística fuera introducida; ahora un cambio en el código que llamé está rompiendo esta suposición.

Esto es el núcleo de por qué gevent causa bugs: la intercalación mágica de código que no se supone que se debía de intercalar causa bugs. Esto no pasa solo en Python; este problema es bien común en lenguajes con hilos; por ejemplo, el HashMap de Java no es seguro para su uso con hilos.

Mitigando problemas de exactitud

Al enfrentarse a estos problemas, comúnmente el instinto es usar bloqueos; rara vez esta es la solución correcta. Los bloqueos crean aún más problemas de desempeño y de exactitud y deberían ser usados sólo como un último recurso.

“ Un programador tenía un problema. Pensó para sí, '¡ya sé, lo resolveré con hilos!'. tiene Ahora problemas. dos"

Algo que PHP hizo bien fue “no compartir estado” — es casi imposible compartir estado accidentalmente entre solicitudes, como sucede aquí. El primer paso para la mitigación del problema es prevenir estados compartidos tanto como se pueda. En el ejemplo anterior deberíamos ya sea crear un nuevo Banco en cada solicitud o usar una base de datos externa. Cada solicitud debe no tener estado.

Si eso no fuera posible, debemos garantizar que todas las actualizaciones al estado sean consistentes. En este caso, la raíz del problema es la suposición de que las lecturas y escrituras son consistentes y que la base de datos no cambió entre unas y otras. Una forma más correcta de hacer esto sería acumular las transacciones en una bitácora, reproduciéndolas cuando hagamos solicitudes a la base de datos. Agregar cosas al final de la bitácora es seguro, dado que nunca estamos modificando la base de datos como tal.

{'rodolfo': 100, 'juan': 100, 'nacho': 100}

Si no es posible evitar estados compartidos, debemos identificar las secciones críticas de código y asegurarnos de que no haya cambios de contexto cuando leemos o escribimos este estado compartido. gevent tiene una utilería gevent.util.assert_switches que hace lo contrario de lo que queremos, pero que podemos manipular para que nos sirva:

Exception                                 Traceback (most recent call last)
<ipython-input-9-767ee5373c1f> in <module>
13 print('¡Prueba exitosa!')
14
---> 15 prueba_enviar_dinero_circularmente()
<ipython-input-9-767ee5373c1f> in prueba_enviar_dinero_circularmente()
9 pass
10 else:
---> 11 raise Exception('¡Cambio de contexto inesperado!')
12 assert b.bd == {'rodolfo': 100, 'juan': 100, 'nacho': 100}
13 print('¡Prueba exitosa!')
Exception: ¡Cambio de contexto inesperado!

Conclusión

gevent tiene todos los aspectos negativos de los hilos: el código es difícil de manejar para los ingenieros que deben considerar todos los posibles intercalados. Es fácil escribir bugs que no se reproducen en pruebas unitarias y nos atacan en producción. gevent es increíblemente poderoso, pero debemos estar al tanto de estos problemas y evitarlos tanto como nos sea posible.

¡PHP tiene millones de problemas, pero el estado compartido no es uno de ellos!

¡Si quieres jugar con este código, puedes bajarlo como un Jupyter Notebook (en inglés)! ¡Practica con el código y mira los problemas por tu propia cuenta!

Esta publicación está inspirada fuertemente en Unyielding, de Gliph.

Esto es parte de una serie de 4 partes. Te sugerimos que también consultes la Parte 1: Qué es gevent, la Parte 3: Rendimiento y la Parte 4: Aplicar los aprendizajes para brindar valor a los usuarios .

¡Lyft está contratando! Si te interesa mejorar la vida de la gente con el mejor transporte del mundo, ¡únete a nosotros!

--

--