Blog ·
Una pausa que mantenía abierto el pedido
Una espera dentro de un flujo mantenía abierta la petición del pedido toda su duración. La solución fue un umbral y no una casilla, y el número se midió.
Por Provibr
Un flujo puede contener una espera. Se aprovisiona una máquina, se le da un momento al agente y después se envía el comando siguiente. Hasta esta semana esa espera era una suspensión dentro de la petición que había iniciado el flujo, lo que significaba que un cliente que hacía un pedido se quedaba mirando un indicador de carga durante toda la pausa.
El límite superior antiguo era de cinco minutos. Alguien podía por tanto construir un flujo de aspecto perfectamente razonable cuyo efecto era un formulario de pedido de cinco minutos.
Ahora que una ejecución puede detenerse y ser continuada por un trabajador, la pausa ya no tiene que ocurrir dentro de la petición. El límite superior pasó a treinta días. La pregunta interesante no es el techo, sin embargo. Es dónde está la línea entre una pausa que la petición puede absorber y una pausa que debería suspender la ejecución.
El umbral está medido, no elegido
Treinta segundos.
La pregunta que nos hicimos no fue «¿cuánto puede durar una pausa?», que no tiene respuesta, sino «¿cuánta pausa tolera ya la petición dentro de la cual se ejecuta este flujo?», que sí la tiene. Esa petición es el pedido.
Medido contra lo que ya ocurre dentro de ella: un nodo de acción espera de forma predeterminada sesenta segundos a un comando y puede configurarse hasta trescientos, y una notificación con correo cuesta alrededor de medio segundo por destinatario. Una pausa que añade otro medio minuto encima de eso es defendible. Una pausa que añade un minuto entero no lo es.
Hay aquí una trampa en la que entramos a propósito en lugar de rodearla. La elección cómoda habría sido poner el umbral en el límite superior antiguo de trescientos segundos, porque entonces todos los grafos existentes se comportan exactamente igual que antes y nadie se lleva una sorpresa. Eso también es incorrecto: conserva toda la clase de problema que motivó el cambio. Los grafos con una pausa de entre treinta y uno y trescientos segundos sí se comportan ahora de otra manera. Su pedido termina antes y el resto del flujo va después. Eso es la mejora, no el precio de la mejora.
El techo de treinta días es una regla de producto y no un límite técnico. La maquinaria de espera sostendría una ejecución durante un año sin problema. Lo que no aguanta un año es una organización en la que ya nadie recuerda por qué hay una ejecución abierta.
Por qué es un umbral y no una casilla
La alternativa evidente es un interruptor de «esperar de forma duradera» en el nodo. No lo construimos, y la razón es una pregunta que no tiene respuesta.
Si existiera esa casilla, ¿cuándo se querría tenerla desactivada? La respuesta honesta es nunca. La espera duradera es un detalle de implementación: quien escribe un flujo expresa «hacer una pausa aquí durante este tiempo» y no tiene ninguna opinión sobre si eso cuesta una fila de base de datos. Un ajuste cuyo valor correcto es siempre el mismo no es un ajuste, es algo que se olvidó decidir.
Así que quien escribe teclea un número de segundos y el umbral decide. Por debajo de la línea el recorrido simplemente duerme, dentro de la petición, sin coste alguno. Por encima, la ejecución escribe su continuación en la cola con un momento de despertar y quien llamó recibe respuesta de inmediato.
El paso que acaba apareciendo en el registro de la ejecución lleva una marca de duradero y el tiempo transcurrido realmente medido en lugar del solicitado, porque esos no son el mismo número y la diferencia es la única manera de ver que la cola va con retraso.
Flujos que llaman a flujos, y dos límites comprobados dos veces cada uno
La segunda mitad de este trabajo es un nodo de subflujo: un flujo que ejecuta otro flujo y espera a que termine.
Eso crea dos maneras de construir algo que nunca termina, y se parecen lo bastante como para que resulte tentador comprobarlas juntas. No son lo mismo. Un ciclo es infinito. Una cadena demasiado profunda es finita pero demasiado larga. Si se responde a las dos con una sola función, un ciclo se informa como «demasiado profundo», y quien escribió el flujo se pone a buscar una cadena larga cuando la suya tiene dos flujos.
Así que hay dos preguntas, y cada una se hace dos veces.
Se hacen una vez al guardar un flujo, de forma estática, contra los demás flujos de la organización. Esa comprobación tiene que estar ahí porque es el único sitio donde es posible un buen mensaje de error: puede nombrar el flujo que cierra el bucle.
Se vuelven a hacer durante la ejecución. Esa comprobación tiene que estar ahí porque los flujos se editan de forma independiente. El flujo A se guarda y está bien. Alguien edita después el flujo C, que ahora apunta de vuelta a A. Nada revalidó A. La comprobación estática es una cortesía para quien escribe; la dinámica es la garantía real.
Merece la pena nombrar dos detalles de implementación. Preguntar «¿puede este flujo alcanzarse a sí mismo en al menos un paso?» responde al caso directo y al indirecto en una sola consulta, y el «al menos un» es toda la definición: un camino de cero pasos haría que todo flujo se alcanzara a sí mismo. Y el recorrido de profundidad guarda su propio camino y devuelve infinito cuando se encuentra un flujo que ya está en él, de modo que no se cuelga con exactamente la entrada para la que existe la comprobación de ciclos. Eso importa porque significa que las dos comprobaciones pueden ejecutarse en cualquier orden, lo que significa que nadie tiene que acordarse de una regla de orden.
La profundidad se guarda como una columna en la ejecución hija, lo que va contra nuestra línea habitual de que un valor derivable no es una columna. La clave foránea al padre se queda nula cuando se borra el padre, así que tras un borrado la cadena ya no se puede recorrer hacia atrás para recalcularla. Un valor derivado cuya fuente puede desaparecer no es derivable.
El reloj de reserva es un día, no cinco minutos
Cuando termina una ejecución hija, pone el momento de despertar de la fila de espera del padre en ahora, y el padre se recoge en menos de diez segundos. Ese es el mecanismo.
Hay además un reloj de reserva, para el caso en que eso no ocurra: un contenedor que se cayó a mitad de terminar la hija, una fila que se tocó a mano. Es de veinticuatro horas.
Cinco minutos sería la elección instintiva y es la equivocada. Una hija que está esperando una aprobación puede legítimamente esperar treinta días. Con un reloj corto, el padre despertaría miles de veces a lo largo de ese periodo para comprobar que nada ha cambiado, y cada uno de esos despertares es una petición HTTP de la pasarela al contenedor web. Un día acota el daño de una señal perdida a un día y cuesta como mucho treinta despertares inútiles a lo largo de la espera más larga que el sistema permite.
Una aprobación de la que nadie se entera es una ejecución detenida
Una petición de aprobación puede enviar ahora un correo, y la categoría con la que se envía es propia y no la categoría general de flujos. Eso fue una desviación deliberada del plan.
La categoría general de flujos va a todo el mundo que tenga el módulo de automatización. Una petición de aprobación debería ir a las personas que pueden decidirla de verdad, que son un conjunto distinto y más pequeño. Así que hay un segundo tamiz sobre la categoría: la notificación solo se entrega a los miembros que tienen el permiso de decisión.
Medido en nuestra organización de pruebas: la categoría general de flujos se resolvió en tres destinatarios y la de aprobación en dos. Dos números que difieren son toda la prueba de que el tamiz hace algo.
Un clic que no seleccionó nada
La lección de medición de esta ronda es sobre la prueba de navegador y no sobre el producto.
Nuestro ayudante de navegador tiene un método de clic que despacha una pulsación del ratón y una liberación del ratón. En un botón corriente eso funciona. En un nodo del lienzo de flujos no hace absolutamente nada: el panel de propiedades se quedó en el nodo disparador y el desplegable que queríamos cambiar no existía, lo que se lee exactamente como una funcionalidad que falta.
El lienzo inicia un arrastre al bajar el puntero y llama a setPointerCapture sobre el nodo. Sin un evento previo de movimiento de ratón, y sin el campo buttons puesto en la pulsación, el navegador no produce un clic que el camino de captura reconozca. Añadir esas dos cosas hace que la selección sea inmediata.
Hay un segundo detalle en el mismo ayudante. No hacer clic en el centro de un nodo. El centro de un nodo con puertos queda entre los puntos de los puertos, y esos tienen su propio manejador de puntero para arrastrar un cable hacia fuera. Hacer clic en la esquina superior izquierda de la cabecera en su lugar.
Las dos cosas parecen fallos del producto cuando uno se topa con ellas, y ninguna lo es. Esa es la forma general del problema: una simulación de entrada que es casi correcta falla de una manera indistinguible de que aquello que se intentaba probar esté roto.
Lo que dejaríamos por escrito
- Un umbral gana a una casilla cuando el valor correcto de la casilla es siempre el mismo. Pregúntese cuándo alguien la desactivaría; si no hay respuesta, no construya el interruptor.
- Mida el umbral contra el presupuesto en el que tiene que caber, no contra la funcionalidad que se está limitando.
- No ponga el umbral nuevo en el límite antiguo solo porque así no cambia nada. Que no cambie nada es el problema que se estaba resolviendo.
- Un ciclo y una cadena demasiado larga son errores distintos. Un solo mensaje para ambos manda a la gente al sitio equivocado.
- Valide al guardar para el mensaje y en tiempo de ejecución para la garantía. Los registros vecinos cambian sin revalidar el suyo.
- Una búsqueda que termina sobre su propio camino puede ejecutarse en cualquier orden respecto de la comprobación a la que sirve, lo que elimina una regla de orden que nadie recordaría.
- Un valor derivado cuya fuente puede borrarse no es derivado. Guárdelo.
- Dimensione un reloj de reserva contra la espera legítima más larga, no contra la rapidez con la que a uno le gustaría enterarse.
- Encamine una notificación por quién puede actuar sobre ella, no por el módulo al que pertenece.
- Una entrada simulada que es casi correcta falla como una funcionalidad rota. Compruebe cómo recibe eventos el componente antes de creerse el resultado.
Para que quede claro qué es esto: la verificación de la fusión recorrió 16 de 16 pasos sin ninguna línea de fallo, el conjunto de pruebas web dio 4583 en verde, los conjuntos de paquetes, pasarela y herramientas 7286 en verde, las pruebas del recorrido 75 en verde, las comprobaciones de subflujo 13 en verde, la ejecución de extremo a extremo contra la base de datos de desarrollo 58 en verde, la medición de destinatarios 4 en verde, y la prueba de navegador 31 en verde con un trabajador real de la pasarela, en la que una pausa de noventa segundos se reanudó a los noventa y cinco segundos, se enlazaron una ejecución padre y una hija, se envió exactamente un correo de aprobación, y una cadena de cuatro y una referencia recursiva fueron ambas rechazadas nombrando en el mensaje la cadena culpable. La prueba del editor dio 9 en verde y el barrido de accesibilidad no encontró nada en seis páginas. Sin demostrar: las ramas de agotamiento de intentos y de reclamo caducado, el reloj de reserva de veinticuatro horas, un subflujo dentro de un bucle, una ejecución hija desaparecida, el correo a alguien que no es propietario mediante un envío real, y absolutamente nada contra un hipervisor real.
Del trabajo de plataforma en Provibr. Cómo aprendió el recorrido a detenerse, para empezar, está en toda llamada a sí misma estaba en posición de cola, y la forma del grafo en sí está en un flujo es un grafo, y un grafo debe ser finito.