featured image

Hace 5 años escribí una entrada sobre Cómo se Hizo Dymmer y solo tengo palabras de desaliento para ese programador de hace 5 años que se embarcó con ilusión en el desarrollo de Dymmer: ¿¡En qué estabas pensando!?

Si quieres echar un vistazo rápido al otro artículo puedes hacerlo, no obstante te lo resumo en pocos puntos. Cuando publiqué el anterior artículo me centré en mencionar estos puntos:

  • Nuevo negocio de venta de dominios, definición y propuesta. Se quedó en sus inicios. Eso de hacer productos es duro así que mira a quien lleve los productos de tu empresa y dile "lo siento, sé que tu trabajo es duro". Porque da igual lo que parezca desde fuera, cuando toca hacerlo es dificilísimo.

  • Cómo se hizo. Como su título indica, exploramos los 191 commits, los bocetos hechos en un coworking en Rickmansworth en Reino Unido en 2017, una infraestructura caduca con promesas de cambiar, que nunca llegaron y a tope de integraciones (p.ej. Mailgun, ReCaptcha, Slack o PayPal entre otras).

A lo largo del artículo voy tocando todos los detalles de la implementación, pero me dejé en el tintero lo más importante, lo que no incluí y debí hacerlo desde primera hora: Calidad Interna.

Las bibliotecas parásito

Como vengo diciendo y recalcando en algunos artículos anteriores, la Calidad Interna o la Mantenibilidad es realmente lo que hace posible que un proyecto dure en el tiempo porque es innegable que sufrirá cambios de versiones, abandono de algunas bibliotecas, cambios de tecnología para adaptarse al móvil, al chat, a la IA, vamos, un sinfín de posibilidades. Pero esto es solo posible si se trabajó en mantener una alta cohesión y un bajo acoplamiento tal y como detalla en este artículo Héctor Patricio de TheDojoMx.

Revisando el código y teniendo en cuenta lo escrito anteriormente.

Gestión de Usuarios con Coherence. Si has sentido curiosidad y has hecho clic en el artículo enlazado a Coherence, verás mi entrada de queja sobre los cambios de Phoenix 1.7 y mi código para adaptar Coherence a Phoenix 1.5 y 1.7. Fue un trabajo enorme y aún así no está en sintonía con el nuevo espíritu de Phoenix orientado a los componentes. ¿El problema? Coherence es una biblioteca tipo parásito.

Las bibliotecas parásito son aquellas que no se usan desde tu aplicación sino que se integran con ella haciendo a tu aplicación dependiente de esta biblioteca. En inglés podríamos llamarlo un vendor-lock en toda regla.

Tal fue el problema que la biblioteca fue abandonada por su autor y todos los que usamos esta biblioteca nos vemos en la obligación de ir actualizando y mejorando la misma para no dejar nuestros códigos demasiado desactualizados. Pero es una coste hundido. La biblioteca hace tiempo fue desfasada y en la comunidad se emplean otras soluciones mejores y que sí avanzan al ritmo del framework.

Obviamente, no todo es culpa de Coherence. Si sigues leyendo verás que hay integraciones con proveedores. Desgraciadamente estas integraciones no se abordaron como bibliotecas sino que se integraron en la base de código y esto provoca muchos problemas. El más acuciante es la imposibilidad de compartir el código, que evolucione solo cuando evoluciona el código de la aplicación y que aunque no queramos, finalmente terminará incrementando el acoplamiento.

Respetar las fronteras

Muchas veces un proyecto ha progresado a un nivel en el que es difícil hacer pequeños cambios. Tiene mucho código desorganizado, muchas ideas confusas y nada está como debería. Tenemos dos opciones: comenzar de nuevo de cero con una base de código más cuidada o deshacer la madeja e ir arreglando el desastre.

Comenzar desde cero suele ser una decisión difícil y dependerá de la complejidad del proyecto, la cantidad de líneas de código y el tamaño del equipo. No es lo mismo un proyecto de decenas de miles de líneas de código con un equipo de 100 personas que un proyecto de miles de líneas con una sola persona.

Veamos la complejidad de Dymmer. En el artículo anterior comenté que había 191 commits en el proyecto Dymmer, para ponerlo en perspectiva el commit 191 es del 8 de enero de 2019 y tiene 5.534 líneas de código distribuidas en 83 módulos.

A lo largo de los años e ir modificando actualmente a las nuevas versiones el número ha cambiado a 470 commits a fecha 15 de enero de 2025 y tiene 8.712 líneas de código distribuidas en 103 módulos.

Las características siguen siendo casi las mismas y principalmente los cambios han atendido a mantenimiento y ajuste de algunas características existentes y sin embargo el tamaño ha crecido.

Huelga decir que la última modificación ha sido principalmente para eliminar código, módulos e intentar reducir la base de código pero como he cambiado de Coherence a phx.gen.auth quiere decir que todo lo que había de código antes en la dependencia de Coherence (no computada en el SLOC del proyecto) ahora sí que entra a formar parte del propio proyecto.

En este caso huimos hacia adelante. No hay reescritura. Las reescrituras de código me han enseñado dos cosas:

  • Si vas a hacer algo nuevo en una tecnología diferente que te permite avanzar rápido tiene sentido.
  • Si usas la misma tecnología y el proyecto es muy largo terminarás preguntándote si gastar tu tiempo de esa forma tiene sentido y terminarás abandonando.

En el segundo punto tengo un par de directorios que así lo atestiguan. También te puede pasar con el primer punto, hace tiempo intenté una reescritura del proyecto en Go y afortunadamente la primera piedra en el camino fue casi al comienzo, me hizo entender que aún cambiando de lenguaje, tecnología y forma de pensar, el desastre sería inevitable a la larga.

Entonces, reescribimos. ¿Cómo comenzar?

¿Cómo hacer migraciones y no desfallecer?

El problema a la hora de realizar una migración, aunque sea una nimiedad, algo tan simple como un mantenimiento adaptativo a la nueva versión del lenguaje de programación, hay que hacerlo midiendo el impacto, la complejidad y poco a poco.

Te pongo el ejemplo con Dymmer. En 2019 la interfaz estaba bien, ahora quiero usar Tailwind CSS y los componentes de Elixir e incluso LiveView, ya que estamos incluso DaisyUI. Pero Coherence no permite el uso de componentes y prefija su forma de uso a vistas y plantillas, por lo que habría que:

  • Migrar Coherence
  • Convertir vistas y plantillas en componentes
  • Migrar a Tailwind CSS
  • Convertir los componentes en DaisyUI

No está estudiado cada paso pero a groso modo debería ser algo así. El problema está en intentar crear el proyecto Phoenix desde cero, con phx.gen.auth, DaisyUI y pretender copiar todo lo demás hasta encajar.

Eso no suele funcionar por el motivo de que no es una acción que pueda hacerse en unas horas. Se tardan días. La motivación y la dedicación a la tarea no son constantes y por tanto puedes llegar después de un fin de semana, ver el proyecto y preguntarte "¿Qué estaba haciendo?"

Es un desastre. No te recomiendo proceder de esta forma aunque parezca la más fácil. No lo es.

Lo ideal es partir de la base de código que funciona. La actual. Asegurarte de que tienes suficientes tests. Si no es así, ese es el primer paso. Agregar suficientes tests para comprobar el funcionamiento correcto del proyecto. Después es cuestión de hacer pequeños cambios, comprobar y subir. Integración y despliegues continuos.

Mi diario de migración de momento ha sido tal que así:

  • Eliminar GenStage y usar Phoenix.PubSub. Aunque GenStage está muy bien es mejor emplear el sistema PubSub que trae consigo Phoenix ya que es más fácil, más rápido y distribuido.
  • Eliminar reCaptcha e implementar hCaptcha. Ha sido una decisión necesaria porque reCaptcha ya no es lo que era. Implementar hCaptcha quita muchos dolores de cabeza.
  • Eliminar la API de PayPal de la base de código y crear un nuevo repositorio público para implementar PayPal v2. Incluso ya hay gente interesada y usando la biblioteca por lo que ha sido positivo. Una reducción en la base de código.
  • Eliminar Distillery y usar Elixir Releases. Ha sido una decisión dura porque me gusta la recarga en caliente de Distillery pero la verdad, mirando cómo funciona Distillery y cómo funcionan las Elixir Releases, me doy cuenta de que Distillery tuvo su momento y ya pasó. Esto simplificó y estandarizó el proceso de despliegue.
  • Usar GenServer con Process.send_after/3 en lugar de cronex, otra biblioteca que parece interesante pero que finalmente no hace falta en verdad y puedes programarte en un momento.
  • Usar Telegram para interactuar con Dymmer. Hace tiempo eliminé Slack porque abandoné esa plataforma y quería algo simple. Los bots en Telegram son muy versátiles y de momento parece un acierto total.
  • Eliminar Coherence y utilizar phx.gen.auth en su lugar. Eliminar esa dependencia parasitaria y hacer la generación de código propio. A diferencia de Coherence, phx.gen.auth no deja dependencias, genera un código que pasa a ser tuyo y por lo tanto puedes modificar, eliminar y agregar lo que quieras. Si ellos cambiasen algo da igual, tu código no está ligado al suyo.
  • Eliminar las vistas y generar el nuevo formato de visualización basado en componentes y rutas verificadas.

Hasta este punto, cada línea es una modificación y una subida a producción. Son tareas ya realizadas, ¿cambios a nivel de usuario? Solo el último ya que cambia el diseño de la interfaz al eliminar mucho código JavaScript que posteriormente será sustituido por LiveView.

El problema es ver la cantidad de cambios y que el usuario solo perciba un cambio de plantilla, decepciona pero no nos preocupemos, lo importante es la sensación transmitida de menos fallos y mejor respuesta del equipo a la implementación de correcciones y nuevas características.

Ha sido un trabajo duro pero la forma de trabajar mediante pequeños cambios y subidas a producción ha hecho que la motivación no se desvanezca. Seamos sinceros, el peor momento de un proyecto migrado en forma todo o nada es precisamente el despliegue en el que todo cambia. Cualquier problema tienta a volver a la versión anterior. En este caso no hay necesidad, los cambios son muy pequeños y están muy probados.

Migraciones todo o nada

Deja que haga de veterano y te cuente una pequeña batallita.

Desde 2004 he estado en proyectos donde la mantenibilidad ha sido tan desastrosa que han planteado un reinicio del mismo. Esto es un error. En todas y cada una de las experiencias que he tenido (no han sido pocas) esto ha llevado a desastre. No hay ni un solo proyecto en el que este enfoque funcionase bien.

Como he dicho, al estilo Windows, esto puede parecer ideal. Tiras el código anterior y usas el nuevo. En proyectos como Windows esto puede funcionar porque pueden romper compatibilidad con versiones anteriores. En proyectos continuos donde se presta un servicio online esto no está tan claro. De hecho, muchas páginas no están seguras de sus movimientos y dejan en enlace para volver a la interfaz anterior.

Volver a la interfaz anterior plantea muchas dudas, ¿ha sido un cambio únicamente estético? ¿No han hecho cambios por abajo o la parte de abajo cambió y da soporte a ambas versiones? Además de dudas, cualquier combinación implica mucho más trabajo que simplemente cambiar.

Si el cambio es todo o nada implica realizar una gran cantidad de modificaciones a todos los niveles poniendo en producción un código no probado y potencialmente con errores quitando otro que quizás tenga muy mala mantenibilidad, pero funciona.

Sin embargo, cuando cambiamos pequeños detalles cada vez son cambios muchas veces sin apreciación por parte de los usuarios, una evolución natural y permanente en el tiempo.

La última vez que sugerí una cambio todo o nada no fue hace mucho tiempo y un compañero de trabajo me respondió: tu toma este proyecto como un gran, enorme, plato de espagueti, toma poco a poco y vamos consumiéndolo. En principio no lo veía, pero con el tiempo me di cuenta de que contraintuivamente, es la mejor opción.

Ahora volviendo a Dymmer, veo un proyecto que ha vuelto a la pista, de tener que descartar cambios por ver su imposibilidad de integrarlos, he vuelto a recuperar la fe en el proyecto y tan solo han sido unos cambios mínimos que han comenzado un proceso de cambio que terminará en unos meses sin dejar de estar en producción.

Escalar no es una necesidad

Otra de las peleas que he tenido siempre en puestos de trabajo desde ese 2004 fue construir sistemas con alta disponibilidad, redundancia y sobre todo que escalen. Si entran 10 usuarios debe funcionar igual que si entran 10 millones.

Es un desafío. Siempre lo ha sido. En la mayoría de empresas donde he estado ha habido muchas veces que lo hemos conseguido y otras que ha sido un desastre. Todas las experiencias cuentan de cara a futuro.

Pero en este proyecto, dada la base de usuarios, tener un cluster multi-región de 4 nodos comenzó a ser un poco descabellado. No es ningún secreto, si quieres saber cómo lo monté aquí tienes el artículo sobre PostgreSQL BDR. ¿Lo volvería a hacer? No. De hecho, en este esquema no funcionó bien CockroachDB ni Yugabyte, solo PostgreSQL y sin embargo tenía sus problemas.

El caso es que tras años de tener algunos problemas y al realizar un cambio en la base de datos, hubo un bloqueo imposible de liberar al hacer el primer despliegue y fue la gota que colmó el vaso. Lo sustituí por un PostgreSQL mucho más actual y mucho más rápido.

No obstante, como una región está en EE.UU. tuvo un problema de rendimiento al intentar realizar peticiones a la base de datos remota, ¿solución? Lo cuento en la siguiente sección.

El aprendizaje es que la distribución de la base de datos la hacía principalmente para que cada servidor DNS tuviese la base de datos de forma local y no tuviese que demorar la carga de datos, pero incluso con esta configuración, la base de datos no era todo lo rápida que debería haber sido.

Mi propio DNS

Como comenté, uno de los problemas que surgió es que usé durante mucho tiempo Bind9 en una versión muy antigua y compilada para hacer peticiones a PostgreSQL directamente y esto tiene muchos problemas.

Así que teniendo presente que el protocolo de DNS no es muy complejo y me gusta implementar protocolos, aproveche que estaba haciendo proyectos con Go y desarrollé mi propio servidor DNS. Sinceramente, después de capturar durante un par de días todas las posibles peticiones que se hacen a un servidor autoritario de zona DNS no tuve duda de que sería muy simple.

El cambio que agregué fue simplemente una caché en memoria de las zonas DNS. Esto aceleró enormemente las peticiones ya que cada petición se resuelve directamente contra la memoria y cada minuto, se hace una nueva petición a la base de datos para ver si cambió algún serial y actualizar las zonas que hayan cambiado.

Como tengo 4 servidores DNS terminé quitando un Bind9 y poniendo uno de estos nuevos servidores en modo depuración y echándole un vistazo a los errores y los logs durante una semana. Después cambié otro de modo que quedaron 2 y 2. Finalmente, coincidiendo con el cambio de la base de datos los cambié todos.

El servidor de EE.UU. carga la base de datos remota al arrancar, tarda unos 20 o 30 segundos, es mucho. Pero después responde a las peticiones desde memoria y cuando tiene que revisar las actualizaciones tarda solo uno o dos segundos. Es aceptable.

Conclusiones

Hemos visto los peligros de no tener un proyecto mantenible, el problema de querer hacer migraciones todo o nada y cómo se puede avanzar sin perder la motivación. En mi caso, es bastante fácil porque me he dedicado a hacer esto mismo para muchas empresas preparando planes de migración, señalando los problemas y ayudando a sacar la basura. Hay veces que ha sido muy fácil y una experiencia muy placentera y otras veces que no acabó nada bien.

Pues ahí sigo avanzando con Dymmer y si quieres gestionar tus nombres de dominio o tus zonas DNS con la posibilidad de gestionar buzones de correo y redirecciones, crea una cuenta gratuita y échale un vistazo. Ya contaré en otro momento el problema con el correo electrónico que eso también tiene mucha historia que contar.