Transacciones anidadas
¿Qué pasa cuando un proceso crea una transacción y alguno de los procesos que lanza también crean una transacción?
Ando inmerso en un sistema de reservas bastante elaborado. Confío en encontrar tiempo para ir hablando sobre algunos de los retos que me ha supuesto. De momento voy a abordar este sencillo pero llamativo problema que no me había planteado hasta ahora: ¿Qué pasa cuando un proceso crea una transacción y alguno de los procesos que lanza también crean una transacción?
El caso más sencillo de transmitiros es la clase `Reserva` que se almacena atómicamente: registro de la reserva, registro del pago, registros con los detalles del pago, registros con los detalles de los servicios extra, registro de los horarios, etc. O todo se almacena y guarda una coherencia (horarios y plazas disponibles) y prefiero que no se almacene nada. Hasta aquí es un caso básico.
Pero ¿qué sucede cuando el cliente ha reservado varios servicios al mismo tiempo? la clase `Reservas` crea una transacción y posteriormente llama a cada una de las `Reserva` individualmente para que se almacenen. No interesa decirle a un usuario "te confirmamlos las reservas pares pero las impares las tienes que repetir" es más práctico informar del problema (por ejemplo, otro usuario se adelantó y reservó un horario que él había elegido), entonces dejarle realizar las modificaciones que quiera y después volver a intentar guardar y confirmarle todo su bloque de reservas.
La idea rechazada, no era tan mala pero acabé descartándola. Primero pensé en hacer un contador interno de manera que el primer start transaction
pusiera el contador a 1, el segundo start transaction
lo pusiera a 2, y cualquier commit
o rollback
volverían el contador a 1 pero no se ejecutarían y algún commit
o rollback
posterior devolverían el contador a 0 y sí se ejecutarían.
La idea aceptada, consiste en asignarle un nombre a cada transacción, entonces se recuerda el nombre del primer start transaction
y se ignora cualquier start transaction
, commit
o rollback
cuyo nombre no coincida y -obviamente- cuando el nombre coincide, se ejecuta y se olvida.
Una cuestión a tener en cuenta en ambos casos es recordar los rollback
ignorados, ya que una sub-transacción fallida debería cancelar la superior para que se cumpla el principio de atomicidad.
class MyDatabaseAbstraction {
...
private $tr_name = null; // Transaction Name
private $must_rb = false; // Must Rollback
public function tr_start($name = null) {
$name = $name ?? uniqid();
$this->log('StartTransaction: '.$name);
if($this->tr_name===null) {
$this->tr_name = $name;
$this->log('StartTransaction: Doing '.$this->tr_name);
$ok = $this->query("START TRANSACTION");
if($ok!==true) throw new \Exception('Unable to start transaction.');
return true;
}
$this->log('StartTransaction: Ignoring '.$name);
return false;
}
public function tr_commit($name = null) {
$name = $name ?? $this->tr_name;
$this->log('Commit: Current: '.$this->tr_name);
if($this->tr_name==$name) {
if($this->must_rb) {
$this->log('Commit: Rollback');
$ok = $this->query("ROLLBACK");
if($ok!==true) throw new \Exception('Unable to rollback transaction.');
$this->tr_name = null;
} else {
$this->log('Commit: Doing');
$ok = $this->query("COMMIT");
if($ok!==true) throw new \Exception('Unable to commit transaction.');
$this->tr_name = null;
}
return true;
}
$this->log('Commit: Ignoring');
return false;
}
public function tr_rollback($name = null) {
$name = $name ?? $this->tr_name;
$this->log('Rollback: Current: '.$this->tr_name);
if($this->tr_name==$name) {
$this->log('Rollback: Doing');
$ok = $this->query("ROLLBACK");
if($ok!==true) throw new \Exception('Unable to rollback transaction.');
$this->tr_name = null;
return true;
}
$this->must_rb = true;
$this->log('Rollback: Ignored but enqueued');
return false;
}
}