martes, 17 de marzo de 2015

Procesos de negocio de larga duración.

Cuando se modela un sistema, el cual está constituido por un conjunto de procesos de negocio, no es difícil percatarse de que existen procesos de negocio que tienen una larga duración. Esto nos genera una situación en la que el flujo de trabajo se convierte en una máquina de estados intermedios desconectada en espera de los eventos para continuar hasta su resolución final.


¿Pero que es un proceso de negocio de larga duración?

Un proceso de larga duración es aquel proceso que, para continuar, debe esperar a eventos del sistema que no son lanzados inmediatamente debido a que su generación es realizada por agentes externos. 

Un sencillo ejemplo puede ser el registro y confirmación por correo electrónico de un usuario en una tienda electrónica. Cuando un usuario realiza un proceso de registro se le envía a su buzón de correo un  email con un código de confirmación. Cuando el usuario envía ese código a la tienda electrónica (evento) el registro se ha completado y ya es oficialmente un usuario de la tienda.

Solución chapucera

La PEOR manera de intentar solucionar este problema es utilizar el repositorio de persistencia de negocio para persistir los estados intermedios del flujo de trabajo agregando atributos extra a las entidades persistidas relacionadas con ese proceso. Hacer esto viola cantidad de reglas básicas de un diseño decente; responsabilidad única, bajo acoplamiento, ignorancia del proceso de negocio coordinador y traspaso de las fronteras del contexto son algunas de ellas.

No sólo caemos en el antipatrón de utilizar una base de datos de negocio para la comunicación entre procesos (o entre un solo proceso desconectado con una máquina de estados), si no que además podemos llegar a repartir los valores de la máquina de estados del proceso en varias entidades; y recuperar ese estado para pasarlo al siguiente es costosísimo, liosísimo y absolutamente descontextualizado.

En el peor de los casos incluso tendremos que persistir en el almacén de negocio pseudo-entidades o entidades en potencia que ni siquiera son todavía entidades de negocio de forma oficial; como un usuario pendiente de confirmar su registro en nuestro sistema. No es todavía un usuario y la estamos liando parda si lo insertamos en la tabla "Usuarios", con algún campo que diga que está pendiente de registro, puesto que todas las demás operaciones realizadas por nuestro sistema tendrán que tener en cuenta que el usuario no esté pendiente de registro para operar. Lío, descontrol, caos, convenciones no documentadas, etc., en pocas palabras: inestabilidad, inseguridad y bugs a cascoporro.

Una propuesta mejor

Ya que tenemos eventos debemos hacer un salto al diseño orientado a eventos y modelar explícitamente los procesos de larga duración; en los cuales encapsularemos su estado y su comportamiento según el mensaje que les llegue. Utilizando un patrón Gestor de Procesos contextualizamos e independizamos el caso de uso de nuestro sistema.

Una cosa que tenemos clara es que necesitamos un repositorio de persistencia para almacenar el estado actual del proceso. Esto nos permite recuperar el proceso cuando se origine un evento y realizar las acciones necesarias que cambien el estado de dicho proceso. 

Hacerse esto uno mismo desde cero puede suponer un trabajo titánico. Por suerte, muchos Buses de Servicios contemplan este patrón y nos proporcionan la capacidad de implementarlo rápido y sencillo.

Continuando con el ejemplo del registro de usuario de más arriba voy a poner un sencillo ejemplo de como se implementaría esto con NServiceBus que implementa los patrones mencionados en lo que llaman Saga.

Partimos de los datos que necesita el proceso de larga duración para su propia máquina de estados:

public class UserRegistrationSagaData : ISagaEntity
  {
    public Guid Id { get; set; }
    public string Originator { get; set; }
    public string OriginalMessageId { get; set; }
    public string Email { get; set; }
    public int Ticket { get; set; }
  }

Y creamos una Saga con esos datos, su comportamiento según los eventos y como persistir la saga en el repositorio que tenga configurado el Bus.

 public class UserRegistrationSaga : Saga<UserRegistrationSagaData>,
    ISagaStartedBy<RequestRegistration>,
    IMessageHandler<ConfirmRegistration>
  {
      public override void ConfigureHowToFindSaga()
    {
      ConfigureMapping<RequestRegistration>(saga => saga.Email, message => message.Email);
      ConfigureMapping<ConfirmRegistration>(saga => saga.Ticket, message => message.Ticket);
    }

public void Handle(RequestRegistration message)
    {
      // generate new ticket if it has not been generated
      if (Data.Ticket == 0)
      {
        Data.Ticket = NewUserService.CreateTicket();
      }
      Data.Email = message.Email;
      MailSender.Send(message.Email,
                      "Your registration request",
                      "Please go to /registration/confirm and enter the following ticket: " + Data.Ticket);
      Console.WriteLine("New registration request for email {0} - ticket is {1}", Data.Email, Data.Ticket);
    }
    public void Handle(ConfirmRegistration message)
    {
      Console.WriteLine("Confirming email {0}", Data.Email);
      NewUserService.CreateNewUserAccount(Data.Email);
      MailSender.Send(Data.Email,
                      "Your registration request",
                      "Your email has been confirmed, and your user account has been created");
      // tell NServiceBus that this saga can be cleaned up afterwards
      MarkAsComplete();
    }
  }

Mucha cosa en tan poco código. ISagaStartedBy indica que mensaje de evento es el que comienza el proceso de larga duración, una petición de registro en este caso. IMessageHandler indica que este proceso también tiene que gestionar los mensajes del evento de confirmación de registro. ConfigureHowToFindSaga indica cuál es la clave por la que se va a persistir y recuperar la saga del repositorio para poder recrearla cuando llegue un mensaje. Handle indica las acciones a tomar según la llegada de cada mensaje.

En Handle(RequestRegistration message) generamos el tiket de solicitud al usuario y le mandamos un correo. En Handle(ConfirmRegistration message) realizamos la tarea de confirmación del usuario y creamos una nueva cuenta de usuario en el sistema.

Un simple controlador de una página web nos sirve perfectamente para lanzar los eventos notificándoselos al Bus.

public class RegistrationController : TxBaseController
  {
    readonly IBus bus;
    public RegistrationController(IBus bus)
    {
      this.bus = bus;
    }
    public ViewResult Index()
    {
      return View();
    }
    public RedirectToRouteResult BeginRegistration(string email)
    {
      bus.Send(new RequestRegistration {Email = email});
      return RedirectToAction("Index");
    }
    public RedirectToRouteResult ConfirmRegistration(int ticket)
    {
      bus.Send(new ConfirmRegistration {Ticket = ticket});
      return RedirectToAction("Index");
    }
  }

No hay comentarios:

Publicar un comentario