Saltar al contenido
Development

Backfill de la realidad en un rastreador de sindicación

Por Victor Da Luz
railsrubycidev-logblog-manager

Hace unos días publiqué la interfaz que muestra cuáles de mis posts están en Medium. Después la abrí en producción y decía que ninguno lo estaba. 169 posts, cero entradas de Medium. El modelo de datos estaba bien; a la base de datos de producción simplemente nunca se le había dicho lo que dev ya sabía.

La parte fácil: volver a correr el backfill codificado

El trabajo de reconciliación anterior vivía en rake tasks, no en el historial de la shell de alguien, y eso rindió frutos de inmediato. medium:seed_published y medium:seed_scheduled se publicaron dentro de la imagen de producción, y backfill_medium! solo mejora el estado, nunca lo empeora, así que correrlos contra prod fue un no-evento: 76 posts marcados como publicados, 8 programados, 0 discrepancias de slug. Si alguna vez surge la tentación de hacer un arreglo de datos puntual a mano en una consola, este es el argumento para escribir en su lugar la rake task descartable, el “puntual” corrió dos veces.

En el camino surgió un prerrequisito: producción no tenía clave de API de Postiz, lo que significaba que el job de reconciliación por hora se había estado saltando en silencio desde el despliegue. Pasé la clave desde la base de datos de dev por stdin para que nunca tocara una terminal, argumentos de proceso, ni el historial de la shell.

El reconciliador estaba roto en todos lados y nadie se dio cuenta

El paso cuatro de mi plan era “correr el reconciliador de RSS para obtener las URLs reales”. Se cayó: ArgumentError: not an HTTP URI. El reconciliador construye la URL del feed a partir del campo profile de la integración de Postiz y asumía que era una URL. Postiz devuelve el handle sin más, “vdaluz”. URI.parse("vdaluz") no tiene host, el código construía "vdaluz/feed", y Net::HTTP lo rechazaba. Mismo fallo en dev, lo que significa que el job por hora había estado fallando en todos lados y lo que iba a marcar mis 8 posts programados como publicados cuando salieran al aire no funcionaba en absoluto.

La solución fue pequeña, pero la revisión la mejoró. Mi primera versión mapeaba cualquier profile sin host a un feed de handle de medium.com. La revisión señaló que URI.parse no devuelve host para NINGUNA cadena sin esquema, así que un dominio personalizado guardado como “blog.example.com” se convertiría en silencio en medium.com/feed/@blog.example.com, obtendría un 404, se interpretaría como un feed vacío, y reportaría éxito para siempre. La versión que se publicó solo trata como handles a las cadenas con forma de handle (sin puntos, sin barras), lanza un error con nombre para cualquier cosa no reconocida, y lanza un error ante respuestas que no sean 2xx. Ahora, una suposición equivocada falla ruidosamente en la cola de jobs en vez de deshabilitar la reconciliación en silencio.

Extrayendo URLs de un dashboard renderizado en el cliente

66 de los posts del backfill no tenían URL de Medium, la interfaz mostraba “Published on Medium” sin nada para hacer clic. Medium mató su API, y el RSS solo expone las 10 historias más recientes, así que la lista completa existe en exactamente un solo lugar: el dashboard de historias con sesión iniciada. Guardar esa página da un shell de React de 99KB sin ningún dato de historias. El truco que funcionó:

// on medium.com/me/stories/public, scrolled to the bottom:
copy(document.body.outerHTML)
// then: pbpaste > /tmp/published_dom.html

El DOM renderizado tiene URLs públicas directas para cada historia. Un script de Nokogiri emparejó 76 de 77 historias con posts por título normalizado, los desajustes eran Medium agregando mi nombre a algunos títulos (con un espacio de no separación, por supuesto) y un título que había reescrito desde 2022. La historia 77 es un duplicado exclusivo de Medium que me negué a adivinar. Un script idempotente de runner después, producción tiene enlaces en los 76 posts publicados. Medium responde 403 a cualquier cliente que no sea un navegador, así que verifiqué por procedencia en vez de con curl.

El fallo de CI que era tres fallos disfrazados de uno

Mergear la solución del reconciliador tomó más tiempo que escribirla. La build de CI falló con bundle: command not found, el runner acababa de reubicarse a un contenedor dedicado y se había saltado el paso de configuración del PATH. La solución documentada agregó PATH=...:$PATH al .env del runner, y después de reiniciar el servicio el CI falló más temprano, con tar: command not found. Ahí fue cuando aprendí que el runner de actions no expande variables dentro de .env: los jobs ahora tenían un PATH con una cadena literal de signo de dólar y sin /usr/bin. El mecanismo real es el archivo .path, que contiene el PATH literal completo. Tercer fallo: Errno::ENOENT - mise durante el bundle install, porque el plugin de RubyGems de mise ejecuta mise reshim después de cada instalación de gema, así que el binario de mise también necesita estar en el PATH del job.

Cada solución movía el fallo un paso más adentro del job, que es la barra de progreso más honesta que puede dar el CI. La parte que sigo reaprendiendo: el paso del runbook había “funcionado” antes solo porque nadie había reiniciado el servicio del runner después de editar .env. Una configuración que nunca se recargó es una configuración que nunca se probó.

Lo que sigue

Los 8 posts programados se publican semanalmente hasta fines de agosto; la reconciliación por hora ya corregida debería pasar cada uno a publicado con su URL, sin intervención. Esa es la prueba real de toda esta cadena. La corrección del runbook ya está registrada, y el aprovisionamiento del runner se está codificando en Ansible para que la lección del PATH quede grabada una sola vez en vez de tener que reaprenderse.

Lecturas relacionadas

Development

El botón de commit del editor es un botón de deploy

Confirmar un borrador a main despliega el blog automáticamente. En cuanto eso quedó claro, sync vs. async dejó de ser una cuestión de estilo, más el caso especial de afiliado heredado que un validador nuevo casi rompió.

Leer