jueves, 6 de julio de 2017

Tuneando el control de concurrencia optimista.

Supongamos que tenemos una entidad en nuestro sistema llamada "Cliente" que se compone de los atributos [Nombre - Apellido -  EsVIP]. Si modelamos esto en un sistema de persistencia con concurrencia optimista nos quedaría así:

Nombre    - varchar(50)
Apellido   - varchar(50)
EsVIP      - bit
stamp       - rowvewsion

donde stamp es un valor que siempre cambia cuando la entidad ha sufrido algún cambio en persistencia.

Como aquí hablamos de concurrencia no me voy a meter en el tema del estancamiento de datos. Para eso les remito a la esta entrada. La gestión del estancamiento de datos no debe basarse en el campo stamp.

Los pasos que vamos a seguir serán los siguientes:
  1. Rehidratamos la entidad desde persistencia.
  2. Le aplicamos los cambios respetando las reglas e invariantes del dominio.
  3. Intentamos persistir la entidad aplicando el control de persistencia.

La operación del control de concurrencia es sencilla. Debemos detectar si el campo stamp de la entidad en memoria es igual a la de la entidad en persistencia. Si son iguales, persistimos; si no, debemos adoptar alguna estrategia. Si el punto de entrada es una UI de usuario muchos optan por devolverle un aviso diciendo que algo ha cambiado, que revise y lo intente de nuevo. 
Entonces, si un cliente se convierte en VIP mientras se está intentando cambiar su apellido de soltera al de casada; ¿Tengo que hacer todo el camino hasta la vista, mostrar el error, molestar al usuario y volver a hacer otro roundtrip completo desde el computador del usuario pasando por toda la arquitectura (física y lógica) del sistema cuando lo único que va a hacer el usuario es pulsar de nuevo el botón de Guardar en muchas de las ocasiones? !Vaya desperdicio de tiempo y recursos!
 Bien visto caballero. Esto nos lleva al...

Tuneo nº 1
Realizar un número limitado de reintentos automáticamente. Esto consiste en volver a rehidratar la entidad, aplicarle los cambios de nuevo e intentar persistirla otra vez.

Al volver a rehidratar la entidad nos aseguramos de que la aplicación de los cambios honrará las reglas e invariantes del dominio. Esto sólo se hace cuando detectamos una colisión de concurrencia. En caso de incumplimiento de reglas de dominio no se debe seguir intentando y se debe notificar. Esta estrategia permite un enorme ahorro de tiempo y recursos.
Entonces, si un cliente se convierte en VIP mientras se está intentando cambiar su apellido de soltera al de casada; ¿Tengo que realizar un reintento aunque no tengan nada que ver y no existan invariantes que me limiten? ¡Vaya desperdicio de tiempo y recursos!
Pues sí, es un poco caca seca esto del control de concurrencia usando un campo por elemento (tabla, documento, etc)... En un motor de persistencia relacional a veces se tiene "suerte" o se puede remodelar alguna parte del esquema para que los datos que no requieran invarianza entre ellos estén en otra tabla y tengan su propio campo stamp en algunas operaciones. Pero si se aplica esta estrategia como estrategia de diseño por defecto para todas las operaciones del sistema terminaremos con el peor (y más lento) diseño relacional del mundo. Los motores de persistencia NoSql tampoco se salvan del todo. Aunque estos tienden a modelar de forma más explícita los agregados para las operaciones también se tiende a desnormalizar mucho para evitar el problema SELECT N+1. Al fin y al cabo, aunque tengamos distintos agregados en el sistema para operaciones en diferentes contextos, estos terminan persistidos en un único documento en el motor NoSql. Lo que nos lleva al...

Tuneo nº 2
Vamos a tener que identificar las operaciones que se realizan en la entidad y que datos de esa entidad se necesitan para cumplir las reglas e invariantes de esa operación (¿Le suenan de algo los agregados?). Con esa información podemos crear más de un campo stamp que nos indique solamente si alguno de esos datos ha cambiado. No suele hacer falta crear un stamp para cada operación, suelen poder reutilizarse y/o agruparse para poder cubrir todas las operaciones en los distintos contextos. La cuestión es tener la mínima, pero suficiente, información que nos indique si debemos realizar un reintento automático o podemos persistir aunque algún dato del agregado haya cambiado.

Nombre              - varchar(50)
Apellido             - varchar(50)
FullNameStamp - rowversion
EsVIP                 - bit
VIPstamp           - rowvewsion

Esto, en un complejo sistema que mueve 1 TB de datos mensuales, puede llegar a ahorrar del orden de miles de colisiones concurrentes con el consiguiente ahorro de reintentos.
Espera, espera, espera... cuando se le manda al servidor la entidad con los nuevo valores (Nombre, Apellido, EsVip) estoy sobrescribiendo en memoria el campo EsVIP al rehidratar y luego aplicar los cambios con esos nuevos valores; por lo que estoy deshaciendo el cambio del campo EsVIP y como sólo estoy mirando el campo FullNameStamp no voy a detectar colisión.
Vale caballero. Veo que lo que usted está haciendo es una aplicación CRUD; no un sistema vivo, reactivo y proactivo a partes iguales según las necesidades; y piensa hacer un UPDATE de toda la entidad entera con todos sus campos en memoria y después persistir esa entidad al completo. Déjeme que le comente una cosa: 
Desarrollar software de negocio trata sobre ofrecer herramientas útiles y poderosas a los usuarios, no un editor visual de base de datos con alguna regla de negocio por aquí y por allá.
¿Está usted seguro de que va por el buen camino?

1 comentario:

  1. Rincon del quisquilloso.

    Este es un ejemplo sobresimplificado de la situacion.

    Con respecto a este ejemplo es obvio que lo mejor seria tener una tabla o un documento que solo contenga la lista de clientes que son VIP en vez de un campo booleano en la tabla de clientes. Con esto solo tendriamos que borrar o agregar en esa tabla cuando se realice la operacion de asignar/desasignar VIP's y no habia problemas de concurrencia.

    La solucion que yo propongo es para casos extraordinarios bien identificados en los cuales, de verdad, no se puede hacer nada por solucionarlo de otra forma mas logica y contextualizada.

    ResponderEliminar