Blog ·
Por qué un 200 de Proxmox no es un resultado
Proxmox responde a una clonación con un UPID y sigue a lo suyo. Cómo leer ese identificador, sondearlo con honestidad y separar tarea acabada de tarea correcta.
Por Provibr
Se envía por POST una petición de clonación a un nodo de Proxmox. Vuelve un HTTP 200 y una cadena que parece ruido de línea. En ese instante es cierta exactamente una cosa: la petición fue aceptada. No se ha copiado ningún disco, no existe ninguna máquina, y tampoco ha fallado nada todavía.
Casi toda llamada mutante de la API de Proxmox funciona así, y es un diseño perfectamente razonable para operaciones que tardan minutos. Solo se vuelve un problema cuando el código que llama trata el 200 como un desenlace, que es el comportamiento por defecto de todo cliente HTTP jamás escrito.
Así leemos nosotros esas tareas, y estas son las partes que resultaron más afiladas de lo que parecen.
La cadena es un identificador, y está estructurada
Lo que vuelve es un UPID: un identificador separado por dos puntos que nombra una tarea en ejecución. Lleva, entre otros campos, el nodo en el que corre la tarea, el tipo de tarea, la cosa sobre la que opera y el usuario que la inició.
Tres de ellos son portantes.
El nodo es aquel al que hay que pedir el estado. Una tarea pertenece al nodo que la ejecuta, y una pregunta a nivel de clúster no es la misma pregunta. Conviene tomar el nodo del identificador y no de la variable que se tuviera a mano, porque las dos pueden diferir y el caso en que difieren es precisamente el que no se probó.
El tipo dice qué se puso en marcha realmente, que no siempre es lo que se pidió por su nombre. Una copia de seguridad iniciada por el endpoint del nodo vuelve con un tipo de tarea vzdump, y no con algo con forma de invitado.
El objetivo es el identificador del invitado en las operaciones de invitado, y puede estar vacío. Eso no es corrupción. Una tarea puede tratar legítimamente sobre un nodo y no sobre un invitado, así que el código que analiza ese campo tiene que devolver «sin invitado» en lugar de lanzar una excepción o caer en cero.
Nuestro analizador es deliberadamente estrecho en lo que rechaza: exige el prefijo, exige al menos ocho campos separados por dos puntos y exige que el nodo y el tipo no estén vacíos. Todo lo demás puede faltar, porque el formato tiene que seguir funcionando para tipos de tarea que nunca hemos visto.
Terminada no es lo mismo que correcta
Al pedir a un nodo el estado de una tarea se obtienen dos campos que responden a dos preguntas distintas. Uno dice si la tarea sigue en marcha o se ha parado. El otro, presente solo una vez parada, dice OK o lleva una cadena de error.
Escribir eso como dos predicados separados en lugar de uno no es pedantería. Un único isDone() que los confunda produce el fallo en que una clonación fallida se informa como clonación terminada, y la máquina que no existe recibe una fila en la base de datos. Tenemos dos funciones: una pregunta si se paró, la otra pregunta si se paró y el estado de salida es exactamente OK. Cualquier otra cosa es un fallo, incluidos estados de salida que nunca hemos visto, porque un estado que no reconocemos no es permiso para suponer éxito.
Dónde vive la tarea es parte de la API, no un detalle
Esto nos lo enseñaron las copias de seguridad. Es natural esperar que una operación de invitado cuelgue del invitado: algo bajo la ruta de la propia máquina virtual. No existe tal endpoint. Una copia se inicia en el nodo, con el identificador del invitado como parámetro.
Eso no es una rareza que haya que esquivar. Cambia lo que hay que saber antes de poder empezar: qué nodo, qué almacenamiento de ese nodo, y si a ese almacenamiento se le permite siquiera guardar copias. Un almacenamiento que solo acepta imágenes de disco aceptará la petición encantado y fallará dentro de la tarea, lo que le llega al cliente como un mensaje de error de un programa del que nunca ha oído hablar. Así que filtramos la lista de destinos por lo que el almacenamiento declara que puede contener, y descartamos cualquier almacenamiento que esté listado pero no responda, antes de ofrecerle a nadie una elección.
La misma regla al revés vale para colocar un disco. La misma idea, el tipo de contenido opuesto, y conviene mantenerlas como dos filtros explícitos y no como uno solo compartido e ingenioso.
El sondeo es un ritmo, no un intervalo
Mientras la tarea corre hay que preguntar repetidamente. Las opciones ingenuas son las dos malas: sondear rápido para siempre y se está machacando la API de gestión de otra persona durante una hora, sondear despacio y toda operación rápida se siente pesada.
Nuestro compromiso son tres intentos rápidos con un segundo de separación y después uno cada tres segundos. Arrancar una máquina termina dentro de la ventana rápida y se siente instantáneo. Una copia de una hora cuesta una petición cada tres segundos, que no es nada.
El bucle termina de exactamente tres maneras, y las tres son explícitas: la tarea informa de que se paró, vence el plazo del comando, o se cancela el comando entero. No hay una cuarta rama ni un camino en el que el bucle simplemente siga, que es una propiedad que conviene organizar a propósito en lugar de esperarla. Lo que un plazo detiene y lo que no es asunto aparte; aquí basta con decir que el bucle de sondeo tiene que tener una forma de perder.
La mayoría de las tareas no dicen por dónde van
Esta es la parte que decepciona a todo el que construye una barra de progreso. El estado de la tarea da en marcha o parada. No da un porcentaje.
Algunas tareas escriben el progreso en su registro, como texto. Una clonación emite líneas con una cuenta de bytes transferidos y un porcentaje entre paréntesis. Así que para esas leemos la cola del registro y tomamos el último porcentaje que encontramos. Sacar un porcentaje de una línea de registro no es elegante, y es el único sitio donde el número existe.
Para todo lo demás mostramos una estimación, y aquí importa más la decisión de diseño que la fórmula. La nuestra se acerca asintóticamente al noventa por ciento desde una duración esperada, de modo que la barra siempre se mueve y nunca llega al final. Una barra parada en el cien por cien mientras no pasa nada es peor que ninguna barra, porque le dice a quien mira que ya está, y después se contradice.
La duración esperada que alimenta esa curva es un parámetro de forma, no un plazo. La nuestra dice que una copia suele tardar unos diez minutos y una restauración unos veinte, con el razonamiento de que una restauración lee el archivo y escribe los discos, y por tanto mueve aproximadamente el doble de bytes. Si una copia dura más que la estimación no se rompe nada; la curva simplemente se queda educadamente por debajo de la línea.
El error está en el registro, no en el estado
Cuando una tarea falla, el estado de salida suele ser casi inútil. «unable to create VM» es una categoría, no una causa. La causa está en las últimas líneas del registro de la tarea.
Así que se monta un informe de fallo: el estado de salida, más la cola del registro con las líneas de progreso filtradas, acotado a un puñado de líneas y a unos pocos cientos de caracteres. Filtrar las líneas de progreso importa más de lo que parece, porque una clonación que falla puede acabar con veinte líneas de porcentajes de transferencia y el motivo real encima de ellas.
Una regla sobre ese texto, a la que nos atenemos estrictamente: es una cita. Es lo que dijo el hipervisor, y va a nuestros registros y a la pantalla sin cambios. No lo traducimos, no lo arreglamos y no lo parafraseamos en algo más amable. La frase que lo rodea es nuestra y se traduce para quien lee; la parte citada se queda exactamente como la dijo el otro lado, porque es la única cadena de todo el intercambio que un ingeniero de soporte puede buscar.
No plegar las palabras propias dentro de la frase de la máquina
La última lección es la que no esperábamos aprender de un sondeador de tareas.
Nuestro bucle de espera recibía antes una etiqueta de texto libre para la operación y la plegaba dentro de tres frases: algo en curso, algo terminado, algo fallido. Funcionaba, hasta que alguien notó que varias de esas etiquetas estaban escritas en el idioma de nuestra oficina y no en inglés. Los clientes leían frases medio traducidas en sus barras de progreso y, cuando una operación fallaba, esa frase medio traducida quedaba guardada para siempre en el campo de error.
El arreglo no fue corregir las etiquetas. Fue eliminar la posibilidad: la operación es ahora una enumeración, y cada caso lleva identificadores para «en curso», «terminado» y «fallido» en lugar de palabras. Ya no queda texto que equivocar, y la frase se construye en el idioma del lector al final del todo. Mantener honestos esos catálogos resulta ser una disciplina propia.
La lista de comprobación
Para cualquier API que responda a una mutación con un identificador en lugar de un resultado:
- Analizar el identificador, no solo guardarlo. Dice dónde preguntar, y ese dónde no siempre es donde se preguntó.
- Separar terminada de correcta, en el sistema de tipos si se puede.
- Tratar un estado terminal no reconocido como fallo, nunca como éxito.
- Dar al bucle de sondeo un ritmo y una forma garantizada de terminar.
- Mostrar el progreso que se tiene de verdad, y una estimación honesta que nunca pueda confundirse con progreso real.
- Citar al otro lado literalmente en los errores, y construir la frase propia alrededor de la cita en lugar de dentro de ella.
Ninguna de estas cosas es difícil. Las seis son cosas que primero hicimos mal.