lunes, 7 de septiembre de 2009

STM.NET: Software Transactional Memory (ii)

Hola a todos! En el post anterior os comenté algunas cosillas sobre STM.NET, un “experimento” de los DevLabs de Microsoft para introducir conceptos transaccionales dentro de .NET. En este segundo post quiero extenderme un poco más con algunos ejemplos un pelín más elaborados.

Ejemplo 3

En el ejemplo 2 (del post anterior) vimos como lanzar una excepción dentro de una transacción definida por Atomic.Do() hacía un rollback de todos los cambios producidos dentro de la transacción. Vamos a verlo con más detalle.

Empezamos por definir una clase “Cuenta” para permitir ingresar o quitar determinadas cantidades de dinero:

class Account
{
public Account(int saldoInicial)
{
Saldo = saldoInicial;
}
public int Saldo { get; private set; }
public void Ingreso(int i)
{
Saldo += i;
}
public void Dispensacion(int i)
{
Saldo -= i;
if (Saldo < 0)
throw new Exception(
string.Format("Error al sacar {0} eur", i));
}
}

Es una clase normal de .NET, sin ninguna otra particularidad. Ahora vamos a hacer un programilla para permitir ir haciendo transferencias de una cuenta a otra:

class Program
{
[AtomicNotSupported]
static void Main(string[] args)
{
Account cuentaDestino = new Account(0);
Account cuentaOrigen = new Account(100);
Console.WriteLine("=== INICIO ===");
Console.WriteLine("Destino {0} eur", cuentaDestino.Saldo);
Console.WriteLine("Origen {0} eur", cuentaOrigen.Saldo);
int transferir = 0;
do
{
transferir = 0;
Console.WriteLine("Entrar fondos a transferir...");
string s = Console.ReadLine();
int valor;
if (int.TryParse(s, out valor))
{
transferir = valor;
try
{
Console.WriteLine
("=== Transfiriendo {0} eur ===",
transferir);
Transferencia(cuentaOrigen, cuentaDestino,
transferir);
}
catch (Exception ex)
{
Console.WriteLine("ERROR en transferencia: {0} ",
ex.Message);
}
}
Console.WriteLine("=== SALDOS ===");
Console.WriteLine("Destino {0} eur", cuentaDestino.Saldo);
Console.WriteLine("Origen {0} eur", cuentaOrigen.Saldo);
} while (transferir != 0);
}
static void Transferencia(Account origen, Account destino,
int qty)
{
destino.Ingreso(qty);
origen.Dispensacion(qty);
}
}

Más simple el agua… El programa va haciendo transferencias de una cuenta a otra. Aquí teneis la salida del programa después de varias ejecuciones:





=== INICIO ===
Destino 0 eur
Origen 100 eur
Entrar fondos a transferir...
120
=== Transfiriendo 120 eur ===
ERROR en transferencia: Error al sacar 120 eur
=== SALDOS ===
Destino 120 eur
Origen -20 eur
Entrar fondos a transferir...

Al hacer la transferencia de 120, nos salta la excepción (capturada correctamente por la función Main), pero hay dos problemas:



  1. Los 100 euros se han ingresado ya en la cuenta de destino
  2. La cuenta de origen se ha quedado en –20

El punto (1) es debido a como se ha codificado la función Transferencia (que primero hace el ingreso y luego el cargo) y el punto (2) es debido a que la función Account.Dispensacion quita el dinero primero y lanza la excepción después. Vale, con un par de ifs esto se arregla, pero es un buen punto de partida para ver como funciona STM.NET.


Aquí está claro que una transferencia es una operación atómica: O bien se hace el cargo y luego el abono, o bien no se hace nada. Si es una operación atómica, lo ideal es ejecutarla dentro de un Atomic.Do(). Podemos modificar la función Transferencia, para que quede con el siguiente código:

static void Transferencia(Account origen, Account destino, 
int qty)
{
Atomic.Do(() =>
{
destino.Ingreso(qty);
origen.Dispensacion(qty);
});
}

Si ahora ejecutamos el programa la salida que tenemos es:





=== INICIO ===
Destino 0 eur
Origen 100 eur
Entrar fondos a transferir...
120
=== Transfiriendo 120 eur ===
ERROR en transferencia: Error al sacar 120 eur
=== SALDOS ===
Destino 0 eur
Origen 100 eur
Entrar fondos a transferir...

La llamada a origen.Dispensacion genera una excepción que provoca que se haga un rollback. El rollback afecta tanto a la cuenta de destino (a la cual ya se le habían ingresado los 120 euros) como a la cuenta de origen (a la cual ya se le habían quitado los 120 euros, quedando en negativo).


Simple, sencillo y elegante.


Ejemplo 4: Supresión parcial de una transacción


Dentro de una transacción el código que se ejecuta debe estar preparado para ejecutarse en una transacción. Hay métodos que por su naturaleza no pueden ser transaccionales (porque realizan efectos visibles al usuario). P.ej el siguiente código falla:

Atomic.Do(() =>  Console.WriteLine("Hola Mundo"));

Lanza una excepción AtomicContractViolationException indicando que Console.WriteLine se accede dentro de un bloque transaccional pero está marcado con el atributo [AtomicNotSupported] indicando que no se soportan transacciones.


A veces (p.ej. para depurar) nos puede interesar llamar a métodos que no soportan transacciones (y por lo tantos marcados mediante [AtomicNotSupported]). En este caso debemos encapsular las llamadas a este método desde otro método, decorado con [AtomicSupress]. El atributo [AtomicSupress] suspende temporalmente la transacción. En este caso deberíamos tener el siguiente código:

[AtomicSupported, AtomicSuppress]
public void Log([AtomicMarshalReadonly] string msg)
{
Console.WriteLine(msg);
}

El atributo [AtomicSupported] indica que el método Log soporta ser llamado desde una transacción, mientras que el atributo [AtomicSuppress] lo que hace es suspender la transacción de forma temporal, permitiendo así que dentro del método Log se llamen a métodos decorados con [AtomicNotSupported] como Console.WriteLine.


Si el método recibe referencias a objetos creados dentro de la transacción, estas referencias deben ser serializadas (código fuera de transacción no puede acceder directamente a referencias creadas dentro de código transaccional). Por suerte para nosotros de este trabajo se puede encargar STM.NET: sólo debemos decorar el parámetro con el atributo [AtomicMarshalReadonly].


Ejemplo 5: Integración con LTM i DTC


Las transacciones iniciadas con STM se integrarán con LTM y DTC:

Atomic.Do(() =>
{
// Operaciones en memoria
// Operaciones sobre BBDD
// Otras operaciones (p.ej MSMQ)
});

La transacción se inicia como una transacción interna de STM (relativamente poco costosa y cuando sea necesario (p.ej. llamada a BBDD o MSQM) se promocionará a una transacción LTM (que a su vez puede ser promocionada de nuevo a una transacción DTC).


De todos modos esa funcionalidad está todavia en desarrollo y funciona sólo para el caso de MSMQ: no es posible usar (de momento) ADO.NET dentro de Atomic.Do() aunque obviamente es algo que está previsto.


Ejemplo 6: Compensaciones


No siempre es posible hacer un rollback: hay veces que una acción tiene efectos visibles inmediatos y no es posible deshacerlos sin que el usuario se entere. En este caso lo que se hace en el rollback es hacer otra acción que compense la acción que se quiere anular. Esto rompe el concepto de “Isolación” ya que el usuario percibe operaciones que no debería percibir (la priemra acción y la acción de compensación), pero en muchos casos es la única alternativa posible.


STM proporciona soporte para comensaciones, usando el método Atomic.DoWithCompensation(). Este método toma dos delegates, con la acción a realizar y la acción de compensación. La acción a realizar se ejecuta de inmediato y la acción de compensación se ejecutará siempre que sea necesario hacer un rollback de la acción. Ambas acciones se ejecutan dentro de un contexto decorado con [AtomicSupress], por lo que pueden llamar a métodos que no soporten transacciones.


Dicho esto… no he conseguido hacer funcionar Atomic.DoWithCompensation(). Intente lo que intente me da una excepción (System error) y ocasionalmente una NullReferenceException. Incluso el propio ejemplo que viene con STM.NET me da esos errores… igual se me está pasando algo por alto :(


Resumen


Bueno… espero que este post, junto con anterior os hayan servido para comprender un poco que es STM.NET. Recordad que es un proyecto de DevLabs y por lo tanto no es, ni mucho menos, un producto terminado (de hecho, su licencia prohibe usarlo para realizar s/w que deba ir en producción)… El tiempo dirá si algunas, o todas, de sus ideas las terminamos viendo en alguna versión del Framework ;-)


Página principal de STM.NET


STM.NET versus TransactionScope

No hay comentarios: