Smart Client

Cómo crear aplicaciones distribuidas con NHibernate y Rhino Service Bus

Oren Eini

Durante mucho tiempo, me he dedicado de manera casi exclusiva a las aplicaciones web. Luego, le di un lugar a la aplicación Smart Client y cuando comencé a trabajar con esta aplicación, al principio no sabía con certeza cómo enfocar su creación. ¿Cómo hago para controlar el acceso de datos? ¿Cómo establezco una comunicación entre la aplicación Smart Client y el servidor?

Además, ya invertí mucho en un conjunto de herramientas existentes que reducen drásticamente el tiempo y el costo de desarrollo y, realmente, deseaba continuar usando esas herramientas. Me llevó un tiempo comprender los detalles a mi agrado y, durante ese tiempo, seguí pensando en lo mucho más sencillo que sería una aplicación web, simplemente porque ya sabía cómo administrar dichas aplicaciones.

Las aplicaciones Smart Client presentan ventajas y desventajas. Una de las ventajas es que tienen capacidad de respuesta y promueven la interactividad con el usuario. Además, se reduce la carga del servidor al trasladar el proceso a un equipo cliente y los usuarios pueden trabajar, incluso mientras no hay conexión con los sistemas back-end.

Por otro lado, existen desafíos inherentes a las aplicaciones Smart Client, como lidiar con las limitaciones de velocidad, seguridad y ancho de banda del acceso de datos en intranet o Internet. Usted también es responsable de sincronizar los datos entre los sistemas front-end y back-end, de realizar un seguimiento de cambios distribuidos y de resolver los problemas que se presentan al trabajar en un entorno conectado ocasionalmente.

Una aplicación Smart Client, como se expone en este artículo, se puede crear con Windows Presentation Foundation (WPF) o Silverlight. Dado que Silverlight cuenta con un subconjunto de características de WPF, las técnicas y los enfoques que detallo en este artículo son aplicables a ambos.

En este artículo, comencé a planificar y crear una aplicación Smart Client con NHibernate para el acceso de datos y con Rhino Service Bus para una comunicación confiable con el servidor. La aplicación funcionará como cliente para una biblioteca de préstamos en línea, que denomino Alexandra. La aplicación en sí misma está dividida en dos partes principales. En primer lugar, existe un servidor de aplicación que ejecuta un conjunto de servicios (en el que residirá la mayor parte de la lógica de negocios) y obtiene acceso a la base de datos a través de NHibernate. En segundo lugar, la interfaz de usuario de Smart Client facilitará la exposición de dichos servicios al usuario.

NHibernate es un marco de asignación de objetos relacionales (O/RM) designado para que el trabajo con la base de datos relacionales sea tan sencillo como el trabajo con datos en la memoria. Rhino Service Bus es una implementación de bus de servicio de código abierto creada en el Microsoft .NET Framework, que se centra principalmente en la facilidad de desarrollo, implementación y uso.

Distribución de responsabilidades

La primera tarea en la creación de la biblioteca de préstamo es decidir la distribución adecuada de las responsabilidades de los sistemas front-end y back-end. Una forma es centrarse en la aplicación, principalmente en la interfaz de usuario, para que la mayor parte del proceso se lleve a cabo en el equipo cliente. En este caso, el sistema de fondo se usa principalmente como un depósito de información.

En esencia, esto es sólo una repetición de la aplicación tradicional cliente/servidor, donde el back-end se usa simplemente como un proxy para el almacenamiento de datos. Esta es una opción de diseño válida si el sistema back-end es sólo un depósito de información. Un catálogo de libros personal, por ejemplo, puede aprovechar dicha arquitectura, porque el comportamiento de la aplicación está limitado a administrar los datos para los usuarios, sin manipulación de datos en el servidor.

Para dichas aplicaciones, recomiendo usar los servicios WCF RIA o los servicios WCF Data. Si desea que el servidor back-end exponga una interfaz CRUD para el mundo exterior, aprovechar los servicios WCF RIA o los servicios de datos de WCF Data le permite reducir drásticamente el tiempo necesario para crear la aplicación. Sin embargo, aunque ambas tecnologías le permiten agregar su propia lógica de negocios a la interfaz CRUD, cualquier intento de implementar un comportamiento de la aplicación significativo a través de este enfoque podría dar como resultado una aplicación débil e insostenible.

No trataré la creación de dicha aplicación en este artículo, pero Brad Adams mostró un enfoque paso a paso para crear sólo una aplicación usando NHibernate y los servicios WCF RIA en su blog en blogs.msdn.com/brada/archive/2009/08/06/business-apps-example-for-silverlight-3-rtm-and-net-ria-services-july-update-part-nhibernate.aspx.

Pasando al otro extremo, puede elegir implementar la mayor parte del comportamiento de la aplicación en el back-end, y dejar que el front-end se encargue de las inquietudes meramente de presentación. Aunque, a primera vista, esto parece razonable, porque es la manera en que normalmente escribe aplicaciones basadas en la web, significa que no puede aprovechar la ejecución de una aplicación real en el lado del cliente. La administración del estado sería más difícil. Esencialmente, no volverá a escribir una aplicación web con todas las complejidades que esto significa. No podrá cambiar el proceso al equipo cliente ni controlar las interrupciones en la conectividad.

Lo que es peor, desde la perspectiva del usuario, este enfoque significa que usted presenta una interfaz de usuario más lenta ya que todas las acciones requieren de un recorrido ida y vuelta al servidor.

Estoy seguro de que no le sorprenderá que el enfoque de este ejemplo esté, de algún modo, en un punto intermedio. Aprovecharé las posibilidades que ofrece la ejecución en el equipo cliente, pero, al mismo tiempo, los componentes importantes de la aplicación se ejecutan como servicios en el back-end, como se muestra en la Figura 1.

Figure 2 The Application's Architecture

Figura 1 Arquitectura de la aplicación

La solución de muestra está compuesta por tres proyectos que puede descargar desde github.com/ayende/alexandria. Alexandria.Backend es una aplicación de la consola que hospeda el código back-end. Alexandria.Client contiene el código front-end y Alexandria.Messages contiene las definiciones de mensajes que ellos comparten. Para ejecutar la solución de muestra, tanto Alexandria.Backend como Alexandria.Client deben ejecutarse.

Una ventaja del alojamiento del back-end en una aplicación de consola es que le permite simular fácilmente escenarios desconectados simplemente al cerrar la aplicación de consola back-end e iniciarla más tarde.

Falacias de la informática distribuida

Con los fundamentos arquitectónicos a mano, echémosle un vistazo a las implicaciones de escribir una aplicación Smart Client. La comunicación con el servidor se llevará a cabo a través de intranet o Internet. Al considerar el hecho de que la fuente principal para llamadas remotas en la mayoría de las aplicaciones web es una base de datos u otro servidor de aplicación ubicado en el mismo centro de datos (y, con frecuencia, en el mismo bastidor), este es un cambio drástico con varias implicaciones.

Las conexiones intranet e Internet tienen problemas de velocidad, limitaciones de ancho de banda y seguridad. La gran diferencia en los costos de comunicación impone una estructura de comunicación diferente a la que usted adoptaría si todas las piezas principales en la aplicación estuvieran en el mismo centro de datos.

Entre los principales obstáculos que debe enfrentar en las aplicaciones distribuidas, se encuentran las falacias de la informática distribuida. Las falacias son conjuntos de suposiciones que los desarrolladores tienden a hacer cuando crean aplicaciones distribuidas, y que, en última instancia, son falsas. El hecho de confiar en estas suposiciones falsas suele generar una reducción de las capacidades o un alto costo para volver a diseñar y a crear el sistema. Existen ocho falacias:

  • La red es confiable.
  • La latencia es cero.
  • El ancho de banda es infinito.
  • La red es segura.
  • La topología no cambia.
  • Hay un administrador.
  • El costo de transporte es cero.
  • La red es homogénea.

Cualquier aplicación distribuida que no tenga en cuenta estas falacias tendrá problemas con el servidor. Una aplicación Smart Client debe abordar dichos problemas desde el comienzo. El uso de almacenamiento en caché es un tema de gran importancia en dichas circunstancias. Aunque no esté interesado en trabajar sin conexión, un almacenamiento en caché casi siempre es útil para aumentar la capacidad de respuesta de la aplicación.

Otro aspecto que debe considerar es el modelo de comunicación para la aplicación. Pareciera que el modelo más simple es un proxy de servicio estándar que le permite realizar llamadas a procedimiento remoto (RPC), pero este proceso suele provocar problemas. Da como resultado un código más complejo para controlar el estado de desconexión y le exige que administre las llamadas asincrónicas explícitamente si desea evitar el bloqueo en el subproceso de interfaz de usuario.

Fundamentos del Back-End

A continuación, se plantea el problema de cómo estructurar el back-end de la aplicación de manera que brinde un buen rendimiento y un grado de separación de la forma en que está estructurada la interfaz de usuario.

El escenario ideal para una perspectiva del rendimiento y la capacidad de respuesta es hacer una sola llamada al back-end para obtener todos los datos que necesita en la pantalla que se presenta. El problema de hacer lo anterior es que obtiene finalmente una interfaz de servicio que imita la interfaz de usuario de Smart Client. Esto no es conveniente por varios motivos. Principalmente, la interfaz de usuario es la parte más susceptible a cambios en una aplicación. Combinar la interfaz de servicio con la interfaz de usuario de este modo genera cambios frecuentes en el servicio, impulsados por cambios íntegramente de la interfaz de usuario.

Esto, a su vez, significa que la implementación de la aplicación se complicó mucho más. Usted debe implementar el front-end y el back-end al mismo tiempo, e intentar admitir varias versiones al mismo tiempo probablemente aumente la complejidad. Además, la interfaz de servicio no se puede usar para crear interfaces de usuario adicionales o como punto de integración para terceros o servicios adicionales.

Si elige la otra opción, es decir, crear una interfaz precisa y estándar, deberá enfrentar las falacias (una interfaz precisa conduce a una gran cantidad de llamadas remotas, que generan problemas de latencia, confiabilidad y ancho de banda).

La respuesta a este desafío es separarse del modelo RPC común. En lugar de exponer los métodos para llamadas remotas, usemos un caché local y un modelo orientado a la comunicación.

La Figura 2 muestra cómo acumular varias solicitudes desde el front-end hasta el back-end. Esto le permite hacer una llamada remota, pero mantener un modelo de programación en el servidor que no esté estrechamente ligado a las necesidades de la interfaz de usuario.

Figure 2 A Single Request to the Server Contains Several Messages

Figura 2 Una sola solicitud al servidor contiene varios mensajes

Para incrementar la capacidad de respuesta, puede incluir un caché local que puede responder cualquier consulta inmediatamente, lo que genera una aplicación con mayor capacidad de respuesta.

Uno de los puntos que debe considerar en estos ejemplos es qué tipo de datos tiene y los requisitos de actualización para cualquier dato que muestre. En la aplicación Alexandria, me apoyo fundamentalmente en el caché local porque es aceptable mostrar los datos en caché del usuario mientras las solicitudes de la aplicación actualizan los datos del sistema back-end. Otras aplicaciones, como el negocio de las acciones, por ejemplo, probablemente deberían mostrar sólo los datos obsoletos.

Operaciones desconectadas

El siguiente problema que debe enfrentar es la administración de escenarios desconectados. En numerosas aplicaciones, puede especificar que una conexión sea obligatoria, lo que simplemente significa que usted puede mostrar un error al usuario si los servidores back-end no están disponibles. Pero, un beneficio de una aplicación Smart Client es que puede funcionar sin conexión, y la aplicación Alexandria lo aprovecha al máximo.

Sin embargo, esto significa que el caché se vuelve incluso más importante porque se usa tanto para aumentar la velocidad de la comunicación como para brindar datos del caché si el sistema back-end no está disponible.

Hasta ahora, creo que ha comprendido bien los desafíos involucrados en la creación de dicha aplicación, así que avancemos para ver cómo sortear estos desafíos.

Uno de mis temas favoritos se relaciona con las colas

En Alexandria, no existe comunicación RPC entre el front-end y el back-end. En su lugar, como se muestra en la Figura 3, toda la comunicación se maneja por medio de mensajes unidireccionales a través de las colas.

Figure 3 The Alexandria Communication Model

Figura 3 El modelo de comunicación Alexandria

Las colas proveen una manera más bien elegante de resolver los problemas de comunicación identificados más arriba. En lugar de tener una comunicación directamente entre el front-end y el back-end (lo que significa que admitir escenarios desconectados es difícil), puede permitir que el subsistema de colas se encargue de todo.

Usar las colas es muy sencillo. Usted solicita a su subsistema de colas local que envíe un mensaje a alguna cola. El subsistema de colas se apropia del mensaje y se asegura de que llegue a su destino en algún punto. Su aplicación, sin embargo, no espera a que el mensaje alcance su destino y puede seguir llevando a cabo su trabajo.

Si la cola de destino no está disponible actualmente, el subsistema de colas esperará hasta que la cola de destino vuelva a estar disponible nuevamente y, luego, entregará el mensaje. El subsistema de colas generalmente mantiene el mensaje en el disco hasta que se entrega, por lo que los mensajes pendientes llegarán a su destino aun si el equipo de origen fue reiniciado.

Cuando se usan las colas, es fácil pensar en términos de mensajes y destinos. Un mensaje que llega al sistema back-end activará una determinada acción, lo que puede provocar el envío de una respuesta al remitente original. Tenga en cuenta que no existen bloqueos en ninguno de los extremos porque cada sistema es completamente independiente.

Los subsistemas de cola incluyen MSMQ, ActiveMQ, RabbitMQ y otros. La aplicación Alexandria usa colas Rhino (github.com/rhino-queues/rhino-queues), un subsistema de cola de código abierto, con xcopy. Elegí Rhino Queues por la sencilla razón de que no requiere instalación ni administración, por lo que es ideal para usar en muestras y en aplicaciones que se necesitan implementar en muchos equipos. Además, vale la pena mencionar que yo he escrito Rhino Queues. Espero que le guste.

Poner las colas a trabajar

Veamos cómo podemos controlar la obtención de datos para la pantalla principal usando colas. A continuación, podrá ver la rutina de inicialización del modelo de aplicación:

protected override void OnInitialize() {
  bus.Send(
    new MyBooksQuery { UserId = userId },
    new MyQueueQuery { UserId = userId },
    new MyRecommendationsQuery { UserId = userId },
    new SubscriptionDetailsQuery { UserId = userId });
}

Le estoy enviando un lote de mensajes al servidor, solicitando cierta información. Hay algunos aspectos que deben destacarse. La granularidad del mensaje enviado es alta. En lugar de enviar un solo mensaje general como el MainWindowQuery, envío muchos mensajes, (MyBooksQuery, MyQueueQuery y así sucesivamente), cada uno para datos muy específicos. Como se mencionó más arriba, esto le da dos ventajas: puede enviar varios mensajes en un solo lote (lo que reduce las idas y vueltas de la red) y reducir el acoplamiento entre el front-end y el back-end.

RPC es el enemigo

Uno de los errores más comunes en la creación de una aplicación distribuida es ignorar el aspecto de distribución de la aplicación. Con WCF, por ejemplo, es sencillo ignorar el hecho de que está haciendo una llamada a método en la red. Aunque ése es un modelo de programación muy simple, significa que deberá ser extremadamente cuidadoso de no violar una de las falacias de la informática distribuida.

En realidad, es el hecho de que el modelo de programación ofrecido por marcos como WCF es muy similar a uno que usted usa para realizar llamadas a métodos en el equipo local lo que lo conduce a hacer dichas suposiciones falsas.

Una API RPC estándar implica el bloqueo cuando se realiza una llamada en la red, un costo más elevado para cada llamada a método remota y la posibilidad de error si el servidor back-end no está disponible. Ciertamente, es posible crear una buena aplicación distribuida sobre este fundamento, pero requiere mucho más cuidado.

Intentar un enfoque diferente lo lleva a un modelo de programación basado en el intercambio de mensajes explícitos (opuesto a los intercambios de mensajes implícitos comunes en la mayoría de las pilas RPC basadas en SOAP). Ese modelo puede parecer extraño al principio y requiere que cambie su forma de pensar un poco, sin embargo, hacer este cambio reduce significativamente la complejidad de preocuparse por todo.

Mi aplicación Alexandria de ejemplo se crea sobre una plataforma de mensajes unidireccional y hace uso completo de esta plataforma, por lo que la aplicación reconoce el hecho de que es distribuida y realmente lo aprovecha.

Tenga en cuenta que todos los mensajes finalizan con el término Consulta. Esta es una convención simple que uso para identificar mensajes de consulta absoluta que no cambian el estado y esperan cierto tipo de respuesta.

Finalmente, tenga en cuenta que, aparentemente, no logro obtener ningún tipo de respuesta del servidor. Debido a que estoy usando colas, el modo de comunicación es “dispare y olvídese”. Envío un mensaje (o un lote de mensajes) ahora y lidio con las respuestas en una etapa posterior.

Antes de ver cómo el front-end lidia con las respuesta, veamos cómo el back-end procesa los mensajes que acabo de enviar. La Figura 4 muestra cómo el servidor back-end ejecuta una consulta de libros. Y allí, por primera vez, usted puede ver cómo uso tanto el NHibernate como el Rhino Service Bus.

Figura 4 Ejecución de una consulta en el sistema back-end

public class MyBooksQueryConsumer : 
  ConsumerOf<MyBooksQuery> {

  private readonly ISession session;
  private readonly IServiceBus bus;

  public MyBooksQueryConsumer(
    ISession session, IServiceBus bus) {

    this.session = session;
    this.bus = bus;
  }

  public void Consume(MyBooksQuery message) {
    var user = session.Get<User>(message.UserId);
    
    Console.WriteLine("{0}'s has {1} books at home", 
      user.Name, user.CurrentlyReading.Count);

    bus.Reply(new MyBooksResponse {
      UserId = message.UserId,
      Timestamp = DateTime.Now,
      Books = user.CurrentlyReading.ToBookDtoArray()
    });
  }
}

Pero, antes de sumergirnos en el código real que maneja este mensaje, analicemos la estructura en la que se ejecuta este código.

Todo lo que debe saber sobre los mensajes

Rhino Service Bus (hibernatingrhinos.com/open-source/rhino-service-bus) es, como es de esperar, una implementación de bus de servicio. Es un marco de comunicación basado en un intercambio de mensajes de cola unidireccional, inspirado principalmente en NServiceBus (nservicebus.com).

Un mensaje enviado en el bus llegará a la cola de destino, en la que se invocará a un consumidor de mensajes. Ese consumidor de mensajes en la Figura 4 es MyBooksQueryConsumer. Un consumidor de mensajes es una clase que implementa ConsumerOf<TMsg> y el método de consumo se invoca con la instancia de mensaje adecuada para controlar el mensaje.

Probablemente, usted puede deducir del constructor MyBooksQueryConsumer que estoy usando un contenedor de inversión de control (IoC) con el fin de suministrar dependencias para el consumidor de mensajes. En el caso de MyBooksQueryConsumer, esas dependencias son el bus en sí mismo y la sesión NHibernate.

El código para consumo real en el mensaje es bastante sencillo. Usted obtiene el usuario adecuado de la sesión NHibernate y envía una respuesta al creador del mensaje con los datos solicitados.

El front-end además tiene un consumidor de mensajes. Este consumidor en para MyBooksResponse:

public class MyBooksResponseConsumer : 
  ConsumerOf<MyBooksResponse> {

  private readonly ApplicationModel applicationModel;

  public MyBooksResponseConsumer(
    ApplicationModel applicationModel) {
    this.applicationModel = applicationModel;
  }

  public void Consume(MyBooksResponse message) {
    applicationModel.MyBooks.UpdateFrom(message.Books);
  }
}

Esto simplemente actualiza el modelo de la aplicación con los datos del mensaje. Sin embargo, debe tener en cuenta una cosa: el método de consumo no se solicita en el subproceso de la interfaz de usuario. En su lugar, se solicita en subproceso en segundo plano. El modelo de aplicación está enlazado a la interfaz de usuario, sin embargo, por esta razón es que la actualización debe llevarse a cabo en el subproceso de la interfaz de usuario. El método UpdateFrom conoce lo anterior y cambiará al subproceso de la interfaz de usuario para actualizar el modelo de aplicación en el subproceso correcto.

El código para administrar los otros mensajes tanto en el back-end como en el front-end es similar. Esta comunicación es totalmente asincrónica. En ningún punto está esperando por una respuesta del back-end, y no usa el API asincrónico del .NET Framework. En su lugar, tiene un intercambio de mensajes explícito que generalmente ocurre casi instantáneamente; sin embargo, además, puede prolongarse durante un tiempo si está trabajando en modo sin conexión.

Antes, cuando envié las consultas al back-end, sólo le ordené al bus que enviara los mensajes, pero no le especifiqué adónde enviarlos. En la Figura 4, sólo solicité la Respuesta, nuevamente, sin especificar adónde se debía enviar el mensaje. ¿Cómo sabe el bus adónde enviar esos mensajes?

En el caso de enviar mensajes al back-end, la respuesta es: configuración la. En la configuración de la aplicación, encontrará la siguiente configuración:

<messages>
  <add name="Alexandria.Messages"
    endpoint="rhino.queues://localhost:51231/alexandria_backend"/>
</messages>

Esto le indica al bus que todos los mensajes cuyo espacio de nombre comienza con Alexandria.Messages deben ser enviados al extremo alexandria_backend.

Al administrar los mensajes en el sistema back-end, solicitar la Respuesta simplemente significa enviar el mensaje de vuelta a su creador.

Esta configuración especifica la propiedad de un mensaje, esto es, a quién enviar este mensaje cuando se coloca en el bus y adónde enviar una solicitud de suscripción para que usted sea incluido en la lista de distribución cuando se publique este tipo de mensajes. No estoy usando la publicación de mensajes en la aplicación Alexandria, por lo que no trataré ese tema.

Administración de sesión

Ha visto cómo funciona el mecanismo de comunicación ahora, pero hay que analizar ciertas preocupaciones acerca de la infraestructura antes de avanzar. Como en cualquier aplicación NHibernate, necesita alguna manera de controlar el ciclo de vida de la sesión y las transacciones adecuadamente.

La perspectiva estándar para las aplicaciones web es crear una sesión por solicitud, para que cada solicitud tenga su propia sesión. Para una aplicación de mensajes, el comportamiento es casi idéntico. En lugar de tener una sesión por solicitud, usted tiene una sesión por lote de mensajes.

Resulta que esto se controla casi completamente por medio de la infraestructura. La Figura 5 muestra el código de inicialización para el sistema back--end.

Figura 5 Inicio de sesiones de mensajería

public class AlexandriaBootStrapper : 
  AbstractBootStrapper {

  public AlexandriaBootStrapper() {
    NHibernateProfiler.Initialize();
  }

  protected override void ConfigureContainer() {
    var cfg = new Configuration()
      .Configure("nhibernate.config");
    var sessionFactory = cfg.BuildSessionFactory();

    container.Kernel.AddFacility(
      "factory", new FactorySupportFacility());

    container.Register(
      Component.For<ISessionFactory>()
        .Instance(sessionFactory),
      Component.For<IMessageModule>()
        .ImplementedBy<NHibernateMessageModule>(),
      Component.For<ISession>()
        .UsingFactoryMethod(() => 
          NHibernateMessageModule.CurrentSession)
        .LifeStyle.Is(LifestyleType.Transient));

    base.ConfigureContainer();
  }
}

La secuencia de arranque es un concepto explícito en el Rhino Service Bus, implementado por las clases que derivan de AbstractBootStrapper. El programa previo tiene la misma tarea que el Global.asax en una aplicación web típica. En la Figura 5 creo primero un generador de sesión NHibernate, luego instalo el contenedor (Castle Windsor) para proporcionar la sesión NHibernate a partir del NHibenrateMessageModule.

Un módulo de mensajes tiene el mismo propósito que un módulo de HTTP en una aplicación web: controlar todas las preocupaciones interdisciplinarias en todas las solicitudes. Uso el NHibernateMessageModule para controlar la duración de la sesión, como se muestra en la Figura 6.

Figura 6 Control de la duración de la sesión

public class NHibernateMessageModule : IMessageModule {
  private readonly ISessionFactory sessionFactory;
  [ThreadStatic]
  private static ISession currentSession;

  public static ISession CurrentSession {
    get { return currentSession; }
  }

  public NHibernateMessageModule(
    ISessionFactory sessionFactory) {

    this.sessionFactory = sessionFactory;
  }

  public void Init(ITransport transport, 
    IServiceBus serviceBus) {

    transport.MessageArrived += TransportOnMessageArrived;
    transport.MessageProcessingCompleted 
      += TransportOnMessageProcessingCompleted;
  }

  private static void 
    TransportOnMessageProcessingCompleted(
    CurrentMessageInformation currentMessageInformation, 
    Exception exception) {

    if (currentSession != null)
        currentSession.Dispose();
    currentSession = null;
  }

  private bool TransportOnMessageArrived(
    CurrentMessageInformation currentMessageInformation) {

    if (currentSession == null)
        currentSession = sessionFactory.OpenSession();
    return false;
  }
}

El código es muy sencillo: registrar los eventos adecuados, crear y disponer de la sesión en los lugares apropiados, y listo.

Una implicación interesante para esta perspectiva es que todos los mensajes en un lote compartirán la misma sesión, lo que significa que en muchos casos puede aprovechar el caché de primer nivel de NHibernate.

Administración de transacciones

Eso es todo para la administración de sesión, pero, ¿qué pasa con las transacciones?

Una mejor práctica para NHibernate es que todas las interacciones con la base de datos sean controladas a través de las transacciones. Pero no estoy usando las transacciones de NHibernate aquí. ¿Por qué?

La respuesta es que las transacciones son controladas por el Rhino Service Bus. En lugar de hacer que cada consumidor controle sus propias transacciones, el Rhino Service Bus tiene un enfoque diferente. Hace uso del System.Transactions.TransactionScope para crear una transacción individual que abarca todos los mensajes del consumidor en el lote.

Esto significa que todas las acciones tomadas como respuesta a un lote de mensajes (a diferencia de un mensaje individual) son parte de la misma transacción. NHibernate automáticamente le da el alta a una sesión en la transacción del entorno para que cuando esté usando el Rhino Service Bus no tenga que tratar explícitamente con las transacciones.

La combinación de una sesión individual y una transacción individual facilita la combinación de operaciones múltiples en una unidad de transacción individual. Además, significa que se puede aprovechar directamente el caché de primer nivel de NHibernate. Por ejemplo, el siguiente es el código relevante para controlar MyQueueQuery:

public void Consume(MyQueueQuery message) {
  var user = session.Get<User>(message.UserId);

  Console.WriteLine("{0}'s has {1} books queued for reading",
    user.Name, user.Queue.Count);

  bus.Reply(new MyQueueResponse {
    UserId = message.UserId,
    Timestamp = DateTime.Now,
    Queue = user.Queue.ToBookDtoArray()
  });
}

El código real para controlar una MyQueueQuery y MyBooksQuery es casi idéntico. Así que, ¿cuál es la implicación del rendimiento de una transacción individual por sesión para el siguiente código?

bus.Send(
  new MyBooksQuery {
    UserId = userId
  },
  new MyQueueQuery {
    UserId = userId
  });

A primera vista, parece que se necesitarían cuatro consultas para reunir toda la información requerida. En MyBookQuery, una consulta para obtener el usuario apropiado y otra para cargar los libros del usuario. En MyQueueQuery, el caso parece ser el mismo: una consulta para obtener el usuario y otra para cargar la cola del usuario.

El uso de una sola sesión para todo el lote, sin embargo, muestra que usted está usando realmente el caché de primer nivel para evitar las consultas innecesarias, como puede ver en la salida del generador de perfiles de NHibernate (nhprof.com) en la Figura 7.

image: The NHibnerate Profiler View of Processing RequestsFigura 7 Vista del generador de perfiles NHibnerate para procesar solicitudes

Soporte de escenarios conectados ocasionalmente

Tal como está, la aplicación no arrojará un error si el servidor back-end no se puede alcanzar, pero no sería muy útil tampoco.

El siguiente paso en la evolución de esta aplicación es convertirla en un cliente real conectado ocasionalmente al introducir un caché que permite que la aplicación continúe funcionando aun si el servidor back-end no responde. Sin embargo, no usaré la arquitectura de caché tradicional en la que el código de la aplicación hace llamadas explícitas al caché. En su lugar, aplicaré el caché al nivel de la infraestructura.

La Figura 8 muestra la secuencia de operaciones cuando el caché se implementa como parte de la infraestructura de mensajería cuando usted envía un mensaje individual solicitando datos acerca de los libros del usuario.

image: Using the Cache in Concurrent Messaging Operations

Figura 8 Uso del caché en operaciones de mensajería simultáneas

El cliente envía un mensaje MyBooksQuery. El mensaje se envía en el bus mientras que, al mismo tiempo, se le solicita al caché que vea si tiene la respuesta para esta solicitud. Si el caché contiene la respuesta para la solicitud previa, inmediatamente envía el mensaje para que se consuma como si acabara de llegar en el bus.

Llega la respuesta del sistema servidor. Se consume el mensaje normalmente y, además, se coloca en el caché. Aparentemente, esta perspectiva parece ser complicada, pero resulta eficaz en el comportamiento del almacenamiento en caché y le permite ignorar casi completamente las preocupaciones de almacenamiento en caché en el código de aplicación. Con un caché permanente (uno que sobrevive al reinicio de la aplicación), usted puede operar la aplicación completa e independientemente sin requerir ningún dato del servidor back-end.

Ahora, implementemos esta funcionalidad. Supongo un caché permanente (el código de muestra brinda una implementación sencilla que usa serialización binaria para guardar los valores en disco) y define las siguientes convenciones:

  • Se puede almacenar en caché un mensaje si es parte de una solicitud o respuesta de intercambio de mensajes.
  • Tanto el mensaje de solicitud como el de respuesta llevan la clave de caché para el intercambio de mensajes.

El intercambio de mensajes es definido por una interfaz ICacheableQuery con una propiedad individual clave y una interfaz ICacheableResponse con propiedades de clave y marcas de tiempo.

Para implementar esta convención, escribo un CachingMessageModule que se ejecutará en el cliente, interceptando los mensajes de entrada y salida. La Figura 9 muestra cómo se controlan los mensajes entrantes.

Figura 9 Conexiones entrantes para el almacenamiento en caché

private bool TransportOnMessageArrived(
  CurrentMessageInformation
  currentMessageInformation) {

  var cachableResponse = 
    currentMessageInformation.Message as 
    ICacheableResponse;
  if (cachableResponse == null)
    return false;

  var alreadyInCache = cache.Get(cachableResponse.Key);
  if (alreadyInCache == null || 
    alreadyInCache.Timestamp < 
    cachableResponse.Timestamp) {

    cache.Put(cachableResponse.Key, 
      cachableResponse.Timestamp, cachableResponse);
  }
  return false;
}

No sucede mucho en esta etapa; si el mensaje es una respuesta que se almacena en caché, lo coloco en el caché. Pero hay una cosa para tener en cuenta: Controlo los mensajes fuera de servicio, es decir, mensajes que tienen una marca de tiempo anterior que llegan después de los mensajes con marcas de tiempo posteriores. Esto asegura que sólo la última información se almacene en el caché.

El control de los mensajes entrantes y el despacho de mensajes desde el caché es más interesante, como puede ver en la Figura 10.

Figura 10 Envío de mensajes

private void TransportOnMessageSent(
  CurrentMessageInformation 
  currentMessageInformation) {

  var cacheableQuerys = 
    currentMessageInformation.AllMessages.OfType<
    ICacheableQuery>();
  var responses =
    from msg in cacheableQuerys
    let response = cache.Get(msg.Key)
    where response != null
    select response.Value;

  var array = responses.ToArray();
  if (array.Length == 0)
    return;
  bus.ConsumeMessages(array);
}

Reúno las respuestas almacenadas en caché y llamo a ConsumeMessages. Eso hace que el bus invoque la lógica habitual de invocación para mensajes, para que parezca que el mensaje llegó de nuevo.

Tenga en cuenta, sin embargo, que aunque haya una respuesta del caché, el mensaje se envía. El razonamiento es que usted puede proveer una respuesta (almacenada en caché) rápida para el usuario y actualizar la información que se muestra al usuario cuando el back-end responde los mensajes nuevos.

Pasos siguientes

He cubierto los bloques de construcción básicos de una aplicación Smart Client: cómo estructurar el servidor y el modo de comunicación entre la aplicación Smart Client y el back-end. Lo último es importante porque seleccionar el modo de comunicación incorrecto puede dar como resultado falacias de la informática distribuida. Además traté algo sobre lotes y almacenamiento en caché, dos enfoques muy importantes para mejorar el rendimiento de una aplicación Smart Client.

Con respecto al servidor, ha visto cómo controlar las transacciones y la sesión de NHibernate, cómo consumir y responder mensajes desde el cliente y cómo todo se reúne en el programa previo.

En este artículo, me enfoqué principalmente en las preocupaciones de la infraestructura; en la próxima entrega, cubriré los mejores procedimientos para enviar datos entre el servidor y la aplicación Smart Client y los patrones para la administración de cambio distribuido.

Oren Eini (quien trabaja bajo el seudónimo Ayende Rahien) es un miembro activo de varios proyectos de código abierto (NHibernate y Castle entre ellos) y es el fundador de muchos otros (Rhino Mocks, NHibernate Query Analyzer y Rhino Commons, entre otros). Eini además es responsable del generador de perfiles NHibernate (nhprof.com), un depurador visual para NHibernate. Puede seguir el trabajo de Eini en ayende.com/blog.