Glassys corre en una máquina tuya y lo abres desde donde estés: un portátil en la misma red, el móvil a través de un túnel. El enlace entre la pantalla y el servidor no es fiable, lo que significa que la pantalla se pasa buena parte de su vida suponiendo. La semana pasada recorrí todos los sitios donde presentaba una suposición como un hecho.
La actualización que decía «listo» mientras seguía corriendo el proceso viejo
La peor de todas. En Linux el servicio se levantaba con enable --now, que no le hace absolutamente nada a una unidad que ya está corriendo. Así que la actualización hacía pull, compilaba, informaba de éxito y te dejaba hablando exactamente con el código que querías reemplazar.
Ahora es enable más restart, y una actualización lanzada desde la web encola el reinicio para que sea el gateway nuevo el que cierre la ejecución. Pero el arreglo de verdad es que la afirmación se comprueba en vez de darse por buena: la salud se consulta en la dirección y el puerto que usa de verdad la configuración, no solo lo que haya en el entorno, y una actualización falla si el endpoint de salud no reporta el commit nuevo. Ese endpoint y la pestaña de actualizaciones nombran el commit en el que corre el proceso, la pestaña avisa de un reinicio que todavía no ha ocurrido y te ofrece recargar cuando el gateway al que reconectas resulta ser otro.
Alrededor de eso me encontré una cosecha de mentiras más pequeñas en el camino de actualización.
- Una actualización cuyo script se murió, no llegó a arrancar o no volvió de su reinicio se quedaba registrada como «en curso» para siempre, y bloqueaba todas las siguientes por estar ya en marcha. Ahora es un error con su motivo.
- La instalación y la actualización usan el lockfile congelado. Un lockfile reescrito a mitad dejaba el clon sucio, y entonces la web rechazaba todas las actualizaciones posteriores. Si falta pnpm o git, te dice cuál, en vez de «pnpm install failed».
- El servicio apunta a una ruta estable de node — el directorio opt de Homebrew, el alias por defecto de fnm o una variable explícita — y no a una con la versión dentro, que la siguiente release de node invalida.
- El directorio de trabajo de la unidad se escribe sin comillas, porque systemd no las quita en los ajustes de ruta y una ruta con un espacio rompía la unidad entera.
- En macOS, la instalación espera a que launchd termine de quitar el trabajo viejo y reintenta el arranque en vez de fallar en el sitio, y el gateway recorta sus propios logs pasados los 10 MB, porque launchd no los rota nunca.
- El script de instalación corre desde una función llamada en su última línea, así que una descarga cortada a medias no ejecuta nada en vez de ejecutar la mitad.
El mensaje que se mostraba enviado y no lo estaba
Un envío se pintaba de forma optimista y después se olvidaba. Ahora se queda pendiente hasta que el gateway lo devuelve, y si no vuelve en diez segundos, o el gateway lo rechaza, el texto y sus adjuntos regresan al compositor con el motivo. El comportamiento anterior era el fallo que más me molesta en el software de los demás: tu mensaje desaparece y no tienes forma de saber si se entregó.
El socket de debajo también es menos crédulo. Nota un enlace que se ha quedado mudo — sin respuesta a dos keepalives — y reconecta. Volver a la pestaña, o volver a tener red, reconecta al momento o demuestra con un ping que el socket abierto sigue vivo. El reintento tiene jitter y solo vuelve a empezar de cero cuando una conexión ha aguantado de verdad. Y los errores llevan su estado HTTP, así que un 502 al arrancar te dice que el servidor no responde en lugar de ofrecerte la pantalla de login de un servidor que no está contestando.
La transcripción que se repetía, o se cortaba
Al reconectar, el gateway tiene que averiguar qué eventos tiene ya el cliente. Antes lo hacía comparando cadenas JSON contra una segunda lectura del fichero de transcripción, lo que se dejaba fuera un evento persistido justo antes de la instantánea — eso es un token repetido al reconectar — y podía tragarse deltas que resultaran idénticos.
Ahora cada evento persistido lleva un número de secuencia creciente dentro de su hilo, la instantánea dice en cuál termina, y tanto el saludo inicial como el cliente descartan todo lo que esté por debajo. El campo es opcional, así que a los clientes viejos no les afecta.
El problema de fondo era la forma del fichero. Un delta emitido era un abrir, añadir y cerrar, con una línea por token, así que una respuesta larga eran miles de líneas y la instantánea de reconexión — que lleva los últimos 800 eventos — te mostraba solo el final de la respuesta que estabas leyendo. Ahora los deltas consecutivos se juntan en una sola línea, que se escribe cuando llega otro evento, pasados 100 ms, o antes de que algo lea el hilo.
La pantalla, además, recibe el evento antes que el disco, así que una escritura de transcripción que falla se avisa una vez en lugar de congelar la salida que estás mirando. Y ese aviso solo lo limpia una escritura que llegó al disco de verdad: antes lo limpiaba un delta en memoria, y el fallo siguiente te volvía a avisar.
Marcas verdes que no se había ganado
Tres afirmaciones más que he quitado:
- Una herramienta que seguía corriendo cuando la ejecución se cancela o falla pone «Detenida», y no la marca verde de hecho que nunca se ganó.
- El aviso de «esta ejecución se ha quedado muda» se guardaba en la transcripción, así que al reproducir el hilo más tarde te avisaba de una ejecución que había terminado hacía días. Ya no se guarda, se limpia cuando el agente vuelve a hablar o la ejecución acaba, y un segundo silencio tiene su propio aviso.
- Un mensaje rechazado — cola llena, gateway ocupado, alta sin terminar — se le dice al cliente que lo envió, en vez de guardarse como ejecución fallida y empujarse a todos tus dispositivos.
Y la página de alta que se fiaba de cualquiera en loopback
Esta pertenece al repaso de seguridad que conté la semana pasada, y es el mismo error con otro disfraz: una suposición tomada por prueba. El alta inicial reclamaba la instancia para cualquier cliente que llegara por loopback. Detrás de un proxy inverso en la misma máquina — que es justo el montaje que recomienda la documentación — todas las visitas llegan por loopback.
Ahora el gateway escribe un código de alta de un solo uso al arrancar por primera vez y lo registra, el instalador lo imprime con un enlace ya preparado, y el alta lo exige salvo que el navegador esté de verdad en la propia máquina. Los códigos equivocados pasan por el limitador de login. Su vecino es un ajuste de proxy de confianza, para que detrás de un proxy local los límites de login cuenten por la dirección reenviada y no por 127.0.0.1: sin él, cuatro intentos fallidos de cualquiera dejaban al operador fuera de su propia instancia.
Un fallo sin misterio, ya que estaba
No se podía enviar desde el móvil durante una ejecución. Stop sustituía a Enviar en el compositor, y los dispositivos táctiles nunca envían con Enter, así que no había manera de encolar nada. Enviar vuelve a estar al lado de Stop en cuanto hay texto, y que Enter envíe lo decide el puntero y no la presencia de una pantalla táctil, así que los portátiles táctiles y las tabletas con trackpad vuelven a funcionar. En el mismo rincón: un mensaje que empieza por barra solo abre la lista de plantillas mientras nombre una de verdad, así que «/var/log/syslog está lleno» se envía como cualquier otra frase.
El hilo que une todo esto es una regla que merece estar escrita en la pared: no informes de un estado que no has confirmado. «Todavía no lo sé» es barato y a nadie le molesta leerlo. «Listo» cuando no lo está le cuesta una tarde a alguien.
Glassys es público, y la semana que produjo todo esto también.
Comentarios