Análisis forense de un YAGNI

«You Aren’t Gonna Need It» o «No vas a necesitarlo». Una de las reglas que todo aquel que se dedica al desarrollo de software debería tener presente en todo momento. Y no me refiero sólo a programadores o arquitectos, sino también a Product Owners que destrozan su producto llenándolo de funcionalidades banales.

No voy a extenderme mucho en el concepto. Hay mucha literatura por ahí. Sólo quería explicar un caso concreto que me comentaba un buen amigo hace poco. De como uno de esos YAGNIs le explotó en la cara recientemente. Como todos los accidentes, nunca hay una sóla causa, por lo podríamos argumentar que no toda la culpa fue del YAGNI. Pero lo cierto es que si alguien se hubiese planteado la necesidad de esa nueva funcionalidad antes de implementarla, probablemente no estaríamos hablando de esto ahora.

Os pongo en situación. Un sistema promocional requiere aplicar una promoción cuando se produce una determidada condición. Esa promoción siempre tiene un importe fijo. El mismo para todos los clientes y mercados. Nadie de negocio ha planteado nunca cambiar esa condición, no está en ningún backlog, nadie lo pidió y no parece que vaya a cambiar en un futuro próximo.

La cafetera.

Pero resulta que una conjunción lunar hace que un colaborador del equipo muy espabilado, voluntarioso e implicado con el proyecto, aunque algo joven e inexperto en el producto y en las técnicas de programación, tenga una conversación informal con el product owner. Y en lugar de hablar de futbol, se plantean que estaría bien parametrizar ese importe por tipo de cliente y mercado. Y se ponen manos a ello.

La zorra y el avestruz.

El resto del equipo, que está ocupado en otras tareas, no aprecia los riesgos de la modificación. Tampoco se ha hecho un grooming ni se ha planificado en ningún sprint, por lo que el desarrollo pasa bastante desapercibido.

Los spaghetti

Parece que el código del producto no es especialmente sencillo. Esta parte es bastante monolítica y tiene implicaciones con el sistema de pagos y los flujos principales de operación del producto. La modificación inicial era bastante sencilla, básicamente un «cuando pase esto, le ingresas esta cantidad al usuario». Pero al parametrizar esa cantidad por tipo de usuario y mercado, se añade más entropia a la ecuación. Y el código monolítico lleno de casos de uso que violan el principio de responsabilidad única tampoco ayuda.

Las caches las carga el diablo

Mi amigo no puede entrar en muchos detalles, pero resulta que, por circunstancias del producto, averiguar a que mercado pertenece el usuario para entregarle la cantidad que corresponde es tarea complicada computacionalmente. Por lo que alguien en el equipo propone cachear esos datos.

Nuestro voluntarioso programador, después de escuchar las críticas y consejos de un equipo de desarrollo que tampoco tiene muchas ganas de dedicar tiempo al problema, decide hacer su enésimo cambio e implementa esa cache. Y considera que el punto adecuado para guardar esa información es en el flujo de autenticación, ya que, a fin de cuentas, todos los usuarios se autentican en algún momento.

Y aquí tenemos el gran amplificador del problema. Lo que debería ser un caso de uso que ocurre esporádicamente, acaba afectando a uno de los flujos más importantes y usados de cualquier aplicación: el login.

¿Que me decías, cariño?

Cuando un coche sale malo suele decirse que aquello de «seguro que lo fabricaron el lunes a primera hora». En software, estos errores suelen aparecer los viernes, cuando los programadores quieren cerrar sprint y el product owner quiere explicarle al jefe lo listo que es y las cosas chulas que ha implementado el equipo.

Y aquí tenemos a nuestro voluntarioso colaborador pidiendo un code review a sus compañeros un viernes a primera hora de la mañana, todos ellos con el café en mano y pensando en el fin de semana.

Y resulta que la pull request, que ya llevaba varios días desarrollo y cayó en el sprint un poco torcida es aprobada sin demasiadas comprobaciones. Los test pasan, el proceso de integración continuo hace lo suyo y los cambios suben a producción sin que nadie sospeche el infierno que se va a liar en los próximos minutos.

Me gusta el olor de Napalm por la mañana

Lo que podía haber sido un viernes tránquilo se convierte pronto en un pequeño infierno. El error afecta el login de los usuarios, pero no falla siempre, sólo con algunos. La cache alimenta la entropía. No todos los usuarios fallan, y no fallan siempre. A veces falla el login, a veces la aceptación de GDPR, otros usuarios pierden su configuración de idioma. Las consecuencias son algunos clientes enfadados, medio equipo buscando e intentando corregir el problema. Gente dejando de hacer otras tareas importantes pero menos prioritarias, jefes cabreados que huelen la sangre y aprovechan para sacar los trapos rotos.. En resumen, mucha gente enfada por culpa de algo que no debió implementarse nunca.

Conclusiones

Los errores nunca vienen sólos. Por lo general, se acumulan varios pequeños errores que originan un gran problema. En este caso, hay algunas causas evidentes.

  • Tareas sin análisis previo o no «groomeadas».
  • Inexperiencia del desarrollador, tanto en el producto como en las técnicas de ingeniería.
  • Poco apoyo por parte del resto del equipo. Por falta de voluntad, o incluso por desconocimiento de la existencia del riesgo.
  • No construir usando pequeños incrementos de valor. Según parece la rama de desarrollo vivía sóla desde hacía varios dias e incluía múltiples funcionalidades. Eso dificulta las code reviews, molesta las subidas a producción y en general, cabrea al equipo.
  • Malas revisiones de código por parte del equipo. Por la razón que sea, los miembros más expertos del equipo no supieron aportar su conocimiento en la revisión de código.
  • Pero sobretodo, el error principal es la existencia del YAGNI en si mismo.

Es posible que toda la cadena de fallos lleve a otro error similar algún día, pero al menos será porque se estaba intentando aportar nueva funcionalidad.

El número de líneas es un gran determinante sobre la complejidad del software. Y la complejidad de un software tiene costes directos, medidos en euros. Un software complejo genera errores y hace más caro el desarrollo. Es mala idea engordar el código con funcionalidades innecesarias.

En este caso, TODA la funcionalidad que necesitaba negocio en el momento acual era añadir una constante con el valor 10 en alguna lugar del código. Como mucho, parametrizarlo en una configuración. 30 minutos de desarrollo, no más.

Concurrencia no debería ser desorden. Aprendí algo de los mutex

Llevo varios días atascado con un problema en el desarrollo del juego con Golang que me traigo entre manos. Como ya os decía en otro post, el servidor está implementado en Golang usando el motor de físicas Box2D.

El tema es que suele ser consenso entre programadores de juegos que la concurrencia hace perder rendimiento. De echo, la mayoría de juegos corren en un único hilo, a lo sumo en dos o tres. Uno para la física, otro para comunicaciones y alguno más para tareas secundarias.

Como yo soy un cabezón y tengo una fe ciega en la concurrencia que ofrece go con sus goroutinas, me he empeñado en abusar de ella todo lo que pueda. Y ya me he dado con la primera en la frente. Mi juego funcionaba bien con pocos clientes conectados pero se volvía totalmente inestable en cuanto el número de conexiones se acercaba a las 500, volviéndose totalmente injugable a partir de esos parámetros.

Después de días haciendo pruebas y refactorizando, incluso pensando que la implementación de Box2D para Go de ByteArena era inestable, por fin he dado con el problema.

Paralelismo no es desorden, sino coordinación. Paralelismo es descomponer un problema en subproblemas y resolver cada uno de esos subproblemas simultaneamente para llegar a una solución final de manera más rápida.

Y yo no estaba haciendo paralelismo, sino concurrencia. Mis tareas no colaboraban entre ellas, más bien competían por conseguir los resultados de una de ellas.

En mi caso, tengo un hilo de ejecución que va calculando lo que ocurre en el mundo. Básicamente, calcula lo que pasa en un pequeño lapso de tiempo y luego duerme un ratito (un frame).

Por otro lado, tengo N hilos de ejecución, uno por cliente conectado, que estaban consultando el mundo y enviando actualizaciones a los clientes. No se puede consultar el estado del mundo mientras este está actualizándose, por lo que decidí proteger el mundo con un mutex. De esa manera, mientras se actualiza el mundo no se puede consultar. Sólo después de actualizar el mundo, esa goroutina duerme un rato liberando el mutex y permitiendo que otras goroutinas consulten el estado de este.

¿Cual era el problema? Que esto no es paralelismo. Es concurrencia. 501 gorutinas compitiendo por el acceso al mutex de manera caótica. No entraban en el orden esperado, y algunas incluso no eran capaces de entrar en el tiempo necesario, provocando fuertes desincronizaciones entre el cliente y el servidor.

La culpa es obviamente mía y de mi manera de pensar. Pensé que la anarquía podría resolver por si sola todo el problema, pero eso no es cierto cuando tienes tiempos precisos de actualización de los clientes y una carga muy elevada en el servidor.

La solución. Aplicar uno de los mantras de la comunidad go, explicada muy bien en el artículo enlazado.

Do not communicate by sharing memory; instead, share memory by communicating.

En lugar de compartir memoria (el mundo) es el mundo el que debe actualizar las vistas de los clientes cuando sea conveniente. Los clientes, que son goroutinas, simplemente deben esperar a que el mundo les notifique que algo ha cambiado. Por lo general, escuchando por un channel.

Creo que es la primera vez que le veo sentido auténtico a la frase «do not communicate by sharing memory….» . Me jode tener que haberlo descubierto en mis carnes con tantas horas de desarrollo, pero mejor tarde que nunca.

NOTA: Por no extenderme, he decidido no incluir nada de pseudocódigo. Creo que la explicación se entiende relativamente bien. Si algún lector lo pidiese, podría ampliar un poco el post con código o incluso enlazar a la Pull Request con los cambios.


Juegos con Golang. Box2d y Phaser

Golang es un lenguaje perfecto para trabajar en el mundo del backend, gracias a su velocidad de ejecución, sus librerías estáticas y su capacidad para la concurrencia.

Sin embargo, es poco considerado en el mundo del gaming, probablemente por su poco acceso a interfaces gráficas, la dificultad de crear aplicaciones mobile y la mala fama que tienen los garbage collectors en el mundo del gaming.

Tampoco vamos a obviar que entornos como Godot o Unity, donde gran parte del boilerplate se hace a golpe de click y te permiten tenerlo todo bajo control en el mismo entorno, ayudan poco a que la gente se lie con go en el mundo del gaming.

El escenario.

Existen un tipo de juegos que pueden beneficiarse mucho de un lenguaje tan rápido como Golang. Los juegos de tipo multijugador masivo que tan de moda se han puesto.

Este tipo de juegos mantienen gran parte del estado del juego en el servidor. La idea es que todo el juego se mantiene en un server centralizado que se comunica con cada uno de los clientes pasando el estado apropiado. El cliente es quien realiza la magia de hacer que todo vaya fluido y perfecto, pero siguiendo siempre las ordenes que recibe desde el server.

El ejercicio que propongo aquí se basa en un juego tipo slither.io donde unas peonzas controladas por los usuarios rebotan en un mundo que vive en el servidor. Para el motor de física hemos propongo usar el port de box2d que hicieron los amigos de ByteArena

Para renderizar, cambiaremos la foto completamente y usaremos el framework phaser y javascript.

En todo caso, como yo soy un programador de backend, estoy enfocando el problema como un server que proporciona un API a un cliente frontend. Si el rendimiento de Phaser no es adecuado, o necesitamos un cliente no html, siempre podríamos programarlo en Unity, Godot o cualquier otra cosa y seguir usando el mismo backend.

Box2D

Box2D es un motor de física en 2D que funciona verdaderamente bien. Tiene ya unos años, se programó entre el 2006 y 2008 en C++ por Erin Catto. Muchos juegos importantes la han usado y está incluido como motor de física en la mayoria de frameworks. Ha sido portada a prácticamente todos los lenguajes usados en el gaming. La versión de go no está documentada pero no es difícil moverse usando la documentación original escrita para C++. Es un producto tan bueno que apenas ha cambiado con los años.

Phaser

Phaser es un framework javascript fantástico para hacer juegos en html5 accediendo directamente a webgl, aunque también es capaz de hacer fallback a canvas.

La capacidad de acceso a WebGL permite a los navegadores usar las funciones de renderizado de la tarjeta gráfica. A pesar de que webGL no nos va a dar lo mismo que Open GL o DirectX, os aseguro que el rendimiento y la cantidad de cosas que pueden hacerse es muy alto, llegando a calidades que uno no imaginaría en un browser y javascript.

Phaser proporciona varios motores de física, y uno de ellos es, Box2D. O sea que tenemos la posibilidad de pensar en 2 mundos diferentes con el mismo motor físico.

De todos modos, apenas usaremos el motor de físicas de phaser. Las colisiones, creación o destrucción de objetos y toda la lógica del juego se realizarán en el server. Phaser se encargará sólo de dibujar. Crearemos objetos que en el server sólo son círculos, pero que en phaser se decorarán ricamente. El server irá actualizando velocidad y posición y phaser se encargará de mover los objetos «aproximadamente» al mismo lugar que dice el server.

A primera vista, usar un framework web puede parecer que nos limite mercado sólo a los navegadores. Pero siempre podemos encapsular esa «web» en un electron, por ejemplo, para tener una aplicación standalone o usar Phonegap para crear una aplicación mobile.

¿Y las comunicaciones? Websockets al rescate.

Ya tenemos el mundo virtual moviéndose en el server y un fantástico framework como Phaser para dibujarlo en el cliente. ¿Pero, como nos comunicamos?

Existe muchísima documentación en el mundo del gaming sobre como implementar comunicaciones. El gaming necesita de mucho ancho de banda y poca latencia, por lo que el consenso general es que TCP es lento y que toda implementación que se precie debe pasar por UDP y técnicas muy esotéticas de gestión de paquetes.

Yo no estoy especialmente de acuerdo en eso. Como el escenario que estamos planteando usa javascript y correrá en un browser, no nos quedan muchas alternativas de comunicación.

Podemos usar llamadas HTTP vía ajax, pero eso nos va a complicar la vida terriblemente, dado lo ineficaz del protocolo para datos pequeños. Tampoco tendríamos una manera sencilla de implementar comunicaciones a demanda del servidor, o hacer broadcasting a todos los clientes.

La solución pasan por los websockets. Estos nos permiten abrir un canal de comunicación TCP a partir de una primera llamada HTTP. La comunicación es óptima, permitiendo enviar datos binarios, y además es bidireccional, es decir, el servidor puede enviar el dato que quiera cuando quiera.

El único problema es la latencia, especialmente en redes móviles o en WIFI. En mi opinión, este es un problema con el que tendremos que vivir. Deberemos diseñar el juego de manera que una latencia de 10 o 20ms no sea un problema para la experiencia de juego.

Juegos como slither.io o agar.io están usando websockets para sus comunicaciones y su experiencia de usuario es más que correcta en la mayoría de ocasiones.

Otra ventaja de los websockets es que tanto Golang como javascript manejan muy bien el protocolo, permitiendo usar unas comunicaciones estándar que luego nos van a dar mucha flexibilidad para implementar nuevos clientes.

En un principio usaremos jsons para codificar mensajes. No es lo más óptimo, dado su elevado peso en datos binarios, pero es legible y fácil de procesar por javascript. No soy muy amigo de las early optimizations, o sea que primero llevaremos json al límite y luego buscaremos una mensajería más compacta.

Pero.. ¿Funciona?

Y tanto que funciona. No sólo eso. Tengo una versión casi implementada que pronto os enseñaré y probablemente diseccionaré en este blog.

No considero que el código esté en absoluto presentable aún, por lo que no voy a enlazarlo. Aunque los más espabilados quizás son capaces de encontrarlo en mi github, mezclado en alguno de los proyectos.