La Lock API de Moodle introduce un mecanismo de exclusión mutua orientado a coordinar ejecución entre procesos concurrentes. Revisando el core, aparece principalmente alrededor de tareas batch, operaciones costosas y estados transitorios asociados a ejecución.

Esta API se incorporó al core alrededor de Moodle 2.6, dentro de un proceso más amplio orientado a mejorar el comportamiento de cron, tareas concurrentes y ejecución distribuida. Parte del problema en aquel momento estaba relacionado con evitar ejecuciones simultáneas de procesos sensibles dentro de instalaciones cada vez más grandes y distribuidas.

Uno de los aspectos más interesantes de la Lock API es cómo el propio core delimita su responsabilidad dentro del sistema. La Lock API queda asociada a coordinación operacional, mientras que la integridad estructural de los datos sigue descansando sobre la base de datos y sobre mecanismos transaccionales.

Qué introduce realmente la Lock API

La abstracción gira alrededor de \core\lock\lock_factory, que define una interfaz común para distintas implementaciones concretas de locking. El core incorpora implementaciones basadas en MySQL named locks, PostgreSQL advisory locks, una implementación genérica apoyada sobre tablas de locking y otra apoyada en file locking. Las garantías operacionales y el comportamiento frente a fallos no son equivalentes entre implementaciones, especialmente en entornos distribuidos o multi-nodo.

Si se necesita otro backend distinto, debe desarrollarse una implementación propia de la factory correspondiente. Existen implementaciones externas siguiendo este enfoque, como local_redislock de Open LMS basada en Redis. En este tipo de implementaciones, además de instalar el plugin correspondiente, es necesario configurar explícitamente la clase de locking en config.php para que Moodle utilice esa factory concreta.

La API está orientada principalmente a coordinación de ejecución entre procesos concurrentes y no a garantizar integridad estructural de datos.

Coordinación de procesos concurrentes

Uno de los escenarios más visibles dentro del core aparece alrededor de cron, scheduled tasks, adhoc tasks y distintos procesos batch.

En estos casos, el objetivo principal consiste en evitar ejecución concurrente sobre operaciones que no deberían solaparse operacionalmente.

Esto puede utilizarse para impedir la ejecución paralela de determinadas tareas, evitar doble procesamiento sobre una misma adhoc task, coordinar runners sobre colas de trabajo y/o limitar contención sobre recursos compartidos.

Protección de operaciones costosas

Procesos como rebuilds, regeneraciones de caché, backups, restores o determinados cálculos masivos introducen una carga suficientemente elevada como para que permitir concurrencia indiscriminada pueda afectar al comportamiento global de la plataforma.

En este tipo de situaciones, el lock funciona principalmente como mecanismo de contención operacional.

La preocupación gira alrededor del impacto que tendría ejecutar simultáneamente varias operaciones con un coste elevado sobre CPU, memoria, IO o carga de base de datos.

Determinados procesos podrían seguir siendo técnicamente correctos desde el punto de vista de integridad de datos y aun así degradar significativamente el comportamiento de la plataforma si se ejecutan concurrentemente.

Lo que el core mantiene fuera de la Lock API

El core mantiene bastante separadas las responsabilidades relacionadas con coordinación concurrente y las relacionadas con integridad estructural de los datos.

La Lock API queda fuera de aspectos como:

  • constraints de base de datos,
  • integridad relacional,
  • claves únicas,
  • transacciones,
  • atomicidad,
  • validaciones persistentes.

La integridad sigue descansando principalmente sobre la base de datos.

Desde el punto de vista arquitectónico, esa separación resulta razonable porque evita desplazar consistencia estructural hacia comportamiento concurrente distribuido, que normalmente resulta mucho más difícil de razonar, observar y mantener con el tiempo.

Uso desde código

El patrón habitual consiste en obtener la factory configurada por Moodle, solicitar un lock asociado a un identificador concreto y liberar ese lock cuando termina la operación.

Un ejemplo desde el core:

public static function get_adhoc_task(int $taskid): ?adhoc_task {
    global $DB;
    $record = $DB->get_record('task_adhoc', ['id' => $taskid]);
    if (!$record) {
        throw new \moodle_exception('invalidtaskid');
    }
    $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
    if ($lock = $cronlockfactory->get_lock('adhoc_' . $record->id, 0)) {
        // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
        try {
            $task = self::adhoc_task_from_record($record);
        } catch (\moodle_exception $e) {
            $lock->release();
            throw $e;
        }
        self::set_locks($task, $lock, $cronlockfactory);
        return $task;
    }
    return null;
}

El segundo parámetro de get_lock() define el tiempo máximo de espera para obtener el lock. En este ejemplo, el timeout 0 convierte la adquisición en no bloqueante. Si otro proceso ya posee el lock, la operación falla inmediatamente devolviendo false.

Mientras el lock permanezca activo, cualquier otro proceso que intente adquirir el mismo identificador quedará bloqueado o fallará dependiendo de la implementación concreta y del timeout configurado.

El identificador del lock resulta importante desde el diseño porque define el ámbito real de exclusión. Un lock excesivamente genérico puede introducir contención innecesaria entre procesos independientes. Uno demasiado específico puede terminar dejando escapar concurrencia que realmente sí debería coordinarse.

También conviene tener presente que el lock está asociado al backend concreto configurado en Moodle ($CFG->lock_factory, revisa el config-dist.php para más información). El comportamiento temporal, persistencia, granularidad o resistencia frente a fallos puede variar entre implementaciones basadas en base de datos, filesystem o soluciones externas.

En procesos largos o especialmente sensibles desde el punto de vista de ejecución, liberar correctamente el lock tiene bastante importancia para evitar bloqueos residuales o esperas innecesarias entre procesos concurrentes.

Los límites reales del locking distribuido

Aunque la Lock API abstrae el mecanismo de coordinación, las garantías reales dependen de la implementación concreta utilizada por cada instalación. No todos los backends presentan el mismo comportamiento frente a caída de procesos, pérdida de conexión, expiración de locks o ejecución distribuida entre múltiples nodos.

Por ejemplo, algunas implementaciones asocian el ciclo de vida del lock a la conexión o proceso, mientras que otras dependen más de mecanismos de expiración o recuperación ante fallos.

Aunque la API está diseñada para liberar explícitamente los locks desde código, el comportamiento real frente a caídas, pérdida de conexión o terminación inesperada del proceso sigue dependiendo parcialmente del backend utilizado.

Esto condiciona bastante cómo deberían diseñarse operaciones largas o especialmente sensibles.

En entornos distribuidos aparecen además problemas clásicos relacionados con coordinación concurrente: locks huérfanos, expiraciones prematuras, timeouts difíciles de ajustar o procesos que continúan ejecutándose tras perder el lock.

Algunas implementaciones, en principio, simples también introducen diferencias de comportamiento. Por ejemplo, soluciones basadas en filesystem pueden comportarse razonablemente bien en nodo único, pero volverse mucho más delicadas sobre NFS o almacenamiento compartido dependiendo del sistema de ficheros subyacente.

Del mismo modo, implementaciones apoyadas en Redis o mecanismos externos de coordinación distribuida añaden dependencias adicionales relacionadas con disponibilidad, persistencia y consistencia del propio backend de locking.

El locking ayuda a coordinar concurrencia, pero no elimina la necesidad de diseñar operaciones razonablemente idempotentes, tolerantes a fallos y apoyadas sobre mecanismos persistentes de integridad.

Una separación arquitectónica sana

Probablemente una de las cosas más interesantes de la Lock API en Moodle sea precisamente esa delimitación clara entre responsabilidades.

La concurrencia aparece tratada principalmente como un problema operacional relacionado con coordinación de ejecución. La integridad estructural sigue descansando sobre la base de datos y sobre los mecanismos asociados a ella.

El resultado es un diseño donde los locks ayudan a coordinar comportamiento concurrente sin convertirse en una capa genérica encargada de sostener consistencia estructural de toda la plataforma.

Si un sistema empieza a depender excesivamente de locking para mantener coherencia funcional, una parte importante de su comportamiento deja de estar representada en estructuras explícitas y verificables, y pasa a vivir en interacciones temporales entre procesos concurrentes, bastante más difíciles de depurar y mantener con el tiempo.

Continúo este tema en Lock API en Moodle II. Calcular, invalidar y publicar resultados, con un caso de reconstrucción de caché que debe coordinarse con las invalidaciones.

Para profundizar