En el primer artículo sobre la Lock API de Moodle repasé la separación entre coordinación de procesos e integridad de datos. En esta segunda parte quiero detenerme en cómo coordinar un cálculo largo con los cambios que pueden dejarlo obsoleto antes de que termine.
Un resumen que queda desactualizado antes de guardarse
Supongamos que una tarea ad hoc prepara un resumen y lo guarda en caché para evitar repetir un cálculo costoso en cada consulta. La tarea lee los datos, calcula el resumen y guarda el resultado.
Mientras la tarea calcula, una petición modifica parte de los datos de origen. El evento correspondiente marca el resumen como obsoleto para que se reconstruya. Sin embargo, la tarea que ya estaba trabajando termina después y guarda su resultado como actualizado.
La invalidación se ha perdido. El resumen recién guardado puede seguir reflejando los datos anteriores al cambio, aunque el sistema lo presente como vigente.
Impedir dos cálculos simultáneos no resuelve esta secuencia. Aquí debemos coordinar una tarea que publica con otra operación que invalida.
Hacer esperar o aceptar que un cálculo se descarte
Una opción consiste en utilizar un mismo lock durante todo el cálculo y exigir que la invalidación lo adquiera también. Así, ambas operaciones se ejecutan por turnos. Si la tarea publica primero, la invalidación posterior marca el resumen como obsoleto.
Es una solución que puede encajar cuando el cálculo es breve y la espera resulta aceptable. El coste aparece cuando la reconstrucción tarda y el evento se procesa dentro de una petición interactiva. Esa petición tiene que esperar al cálculo para completar la invalidación.
Otra opción permite que la invalidación actúe mientras la tarea sigue calculando. Cuando termine, la tarea comprobará si puede guardar su resultado o debe descartarlo porque los datos cambiaron durante su ejecución.
Esta alternativa evita esperar a la reconstrucción completa, a cambio de aceptar que parte del trabajo realizado pueda perderse. Puede ser razonable cuando necesitamos que la petición responda pronto y podemos repetir el cálculo en segundo plano. Si los cambios son continuos, los descartes pueden llegar a ser frecuentes y habrá que valorar ese coste.
Un token y un lock breve
Para detectar esas invalidaciones, la tarea registra un token al comenzar y conserva una copia. Podemos imaginar que su valor es A. Si llega una invalidación, esta sustituye el token registrado por otro valor, B.
Cuando termina el cálculo, la tarea compara su copia con el token registrado. Si encuentra B, sabe que hubo una invalidación desde que empezó y descarta el resultado. Si sigue encontrando A, el cálculo puede publicarse según este protocolo.
Pero todavía hay un intervalo que debemos proteger. Después de que la tarea compruebe que el token sigue siendo A, puede llegar una invalidación que lo cambie a B. Si la tarea guarda el resumen sin comprobarlo de nuevo, publicará el cálculo anterior al cambio.
Un lock breve permite que la comprobación y la escritura se ejecuten juntas, sin que la invalidación se intercale. La invalidación utiliza ese mismo lock para cambiar el token y marcar el resumen como obsoleto. El registro del token inicial también debe coordinarse mediante ese lock.
El cálculo pesado se realiza fuera de esta sección crítica. La petición deja de esperar a la reconstrucción completa, aunque todavía debe adquirir el lock y puede encontrarlo ocupado por una publicación o por otra invalidación.
Los dos órdenes quedan definidos. Si la tarea publica primero, el evento posterior invalida lo que acaba de guardar. Si el evento invalida primero, la tarea encuentra un token distinto y descarta el cálculo.
Si además queremos evitar reconstrucciones simultáneas del mismo resumen, podemos mantener un lock independiente durante el cálculo. Así, la invalidación comparte el lock breve de publicación y puede actuar mientras la reconstrucción continúa. Los locks se asocian al resultado concreto, de modo que otros resúmenes independientes puedan calcularse en paralelo.
La frecuencia también tiene un coste
Una sección crítica breve reduce el tiempo durante el que retenemos el lock. Adquirirlo y liberarlo también tiene un coste, incluso cuando ningún otro proceso lo está utilizando.
La documentación de Moodle advierte que la Lock API prioriza la coordinación correcta, también entre nodos, y desaconseja utilizarla en código que se ejecuta muchas veces dentro de una misma petición. Menciona MUC como ejemplo de ese uso frecuente. Recomendación oficial sobre rendimiento.
Adquirir un lock para evitar que se duplique una reconstrucción costosa puede compensar su coste. En cambio, si un evento afecta a muchos resúmenes y la petición adquiere un lock para invalidar cada uno, la suma de esas operaciones puede penalizar la respuesta aunque las secciones críticas sean cortas.
Por eso, separar cálculo y publicación debe evaluarse también por el número de adquisiciones que introduce. Una clave por resultado permite concurrencia entre reconstrucciones independientes, pero puede multiplicar los locks necesarios para una invalidación amplia.
Si ese recorrido resulta costoso, conviene comparar la invalidación individual con agruparla o utilizar una marca compartida que los cálculos comprueben antes de publicar. Estas alternativas requieren definir qué resultados quedan invalidados y cómo se coordina la comprobación con la escritura. Un ámbito más amplio puede simplificar la invalidación, a cambio de descartar o reconstruir resultados que no habían cambiado.
Para valorar el diseño necesitamos medir cuántos locks adquiere una petición, cuánto cuesta adquirirlos con el proveedor efectivo y cuánto tiempo se pierde esperando por contención. La duración del cálculo, por sí sola, no permite anticipar el impacto sobre la respuesta.
La actualización sigue pendiente
Descartar un resultado obsoleto deja pendiente obtener uno nuevo. El diseño debe conservar esa necesidad de actualización.
La existencia de una tarea en ejecución no basta para darla por cubierta. Esa tarea puede estar calculando con datos anteriores al cambio y terminar descartando su resultado. Una política posible permite una reconstrucción en curso y una solicitud posterior en la cola, que se atenderá cuando pueda adquirir el lock de cálculo.
También debemos definir qué ocurre si no se consigue un lock o falla la escritura. La exclusión mutua coordina las operaciones que participan en el protocolo, pero varias escrituras bajo un lock pueden quedar incompletas si alguna falla. El sistema necesita una forma de recuperarse y volver a solicitar el trabajo pendiente.
Esta solución depende de que los cambios relevantes invaliden el resumen siguiendo el mismo protocolo. El token detecta esas invalidaciones; las garantías sobre cómo se leen los datos y cómo se almacenan los resultados deben resolverse en sus respectivos mecanismos.
La decisión combina cuánto tiempo podemos hacer esperar a una petición, cuántos locks necesita adquirir y cuánto trabajo estamos dispuestos a descartar. Para un cálculo breve, serializar cálculo e invalidación puede ser suficiente. Para una reconstrucción costosa, separar el cálculo de la publicación evita esperar al cálculo completo, siempre que el coste acumulado de los locks resulte aceptable y conservemos las actualizaciones pendientes. Esa mejora debe comprobarse con el patrón de invalidaciones y el proveedor de la instalación.