Cliente inteligente

Criando aplicativos distribuídos com o NHibernate e o Rhino Service Bus

Oren Eini

Durante muito tempo, trabalhei quase que exclusivamente com aplicativos Web. Quando mudei e comecei a criar um aplicativo de cliente inteligente, a princípio fiquei um pouco perdido quanto à abordagem que usaria para isso. Como lidar com o acesso a dados? Como estabelecer comunicação entre o aplicativo cliente inteligente e o servidor?

Além disso, eu já tinha feito um grande investimento no conjunto de ferramentas atual que reduziu drasticamente o tempo e o custo de desenvolvimento e queria mesmo poder continuar usando essas ferramentas. Para meu contentamento, demorei um tempo para perceber os detalhes e, durante esse período, continuei pensando em como seria mais simples um aplicativo Web — se fosse só porque eu já sabia trabalhar com esses aplicativos.

Os aplicativos clientes inteligentes têm vantagens e desvantagens. No lado positivo, os clientes inteligentes são responsivos e promovem interatividade com o usuário. Também se reduz a carga do servidor, porque o processamento é passado para um computador cliente, e os usuários podem trabalhar até mesmo desconectados de sistemas back-end.

Por outro lado, existem desafios que são próprios desses clientes inteligentes, como contender com as limitações de velocidade, segurança e largura de banda do acesso a dados via intranet ou Internet. Você também é responsável por sincronizar dados entre sistemas front-end e back-end, pelo controle de alterações distribuídas e por resolver problemas relacionados ao trabalho em um ambiente ocasionalmente conectado.

Um aplicativo cliente inteligente, conforme discutido neste artigo, pode ser criado com o Windows Presentation Foundation (WPF) ou o Silverlight. Como o Silverlight expõe um subconjunto de recursos do WPF, as técnicas e abordagens que descrevo aqui são aplicáveis a ambos.

Neste artigo, inicio o processo de planejar e criar um aplicativo cliente inteligente usando o NHibernate para acesso a dados e o Rhino Service Bus para uma comunicação confiável com o servidor. O aplicativo funcionará como o front-end de uma biblioteca de empréstimo online, que chamei de Alexandra. O aplicativo propriamente dito é dividido em duas partes principais. Na primeira, há um servidor de aplicativos executando um conjunto de serviços (onde residirá a maior parte da lógica de negócios e acessando o banco de dados por meio do NHibernate. Na segunda, a interface de usuário do cliente inteligente facilitará a exposição desses serviços para o usuário.

O NHibernate é uma estrutura O/RM (mapeamento relacional de objetos) que visa facilitar o trabalho com bancos de dados relacionais, assim como é com os dados em memória. O Rhino Service Bus é uma implementação de barramento de serviço de código-fonte aberto desenvolvida no Microsoft .NET Framework e cujo principal objetivo é facilitar o desenvolvimento, a implantação e o uso.

Distribuição de responsabilidades

A primeira tarefa da criação da biblioteca de empréstimo é escolher a distribuição apropriada da responsabilidade entre os sistemas front-end e back-end. Um caminho é focar o aplicativo basicamente na interface do usuário de modo que a maior parte do processamento seja feita no computador cliente. Nesse caso, o back-end funciona principalmente como repositório de dados.

Em essência, isso é apenas uma repetição do aplicativo cliente/servidor tradicional, com o back-end funcionando como um simples proxy para o repositório de dados. Esta é uma opção de design válida se o sistema back-end é apenas um repositório de dados. Um catálogo de livros pessoal, por exemplo, pode se beneficiar dessa arquitetura, pois o comportamento do aplicativo limita-se a gerenciar dados para os usuários, sem manipulação dos dados no lado do servidor.

Para tais aplicativos, recomendo fazer uso de Serviços WCF RIA ou de Serviços de Dados WCF. Se você quiser que o servidor back-end exponha uma interface CRUD para o mundo exterior, usar os Serviços WCF RIA ou os Serviços de Dados WCF permite reduzir consideravelmente o tempo necessário para criar o aplicativo. Mas, embora as duas tecnologias permitam adicionar sua própria lógica de negócios à interface CRUD, qualquer tentativa de implementar um comportamento de aplicativo importante usando essa abordagem poderia resultar em uma confusão frágil e insustentável.

Neste artigo, não vou falar sobre como criar um aplicativo assim, mas Brad Adams mostrou uma abordagem passo a passo que ensina a criar um aplicativo exatamente como esse usando o NHibernate e Serviços WCF RIA em seu blog: blogs.msdn.com/brada/archive/2009/08/06/business-apps-example-for-silverlight-3-rtm-and-net-ria-services-july-update-part-nhibernate.aspx.

No outro extremo, você pode decidir implementar a maior parte do comportamento do aplicativo no back-end, deixando para o front-end somente os aspectos de apresentação. Apesar de isso parecer razoável em um primeiro momento, pois tem a ver com a maneira como você cria aplicativos baseados na Web, significa que não é possível se beneficiar da execução de um aplicativo real no lado do cliente. O gerenciamento de estado seria mais complexo. Basicamente, você está fazendo write-back de um aplicativo Web, com todas as complexidades que isso implica. Você não poderá mudar o processamento para o computador cliente nem lidar com as interrupções de conectividade.

E o que é pior, da perspectiva do usuário, essa abordagem significa que você apresenta uma interface de usuário mais lenta, já que todas as ações exigem uma viagem de ida e volta até o servidor.

Tenho certeza de que você não ficará surpreso se souber que a abordagem que estou seguindo neste exemplo fica em uma categoria intermediária. Vou me beneficiar das possibilidades oferecidas pela execução em um computador cliente, mas, ao mesmo tempo, partes importantes do aplicativo são executadas como serviços no back-end, como ilustrado na Figura 1.

Figure 2 The Application's Architecture

Figura 1.A arquitetura do aplicativo

A solução de exemplo é composta de três projetos, que você pode baixar em github.com/ayende/alexandria. Alexandria.Backend é um aplicativo de console que hospeda o código do back-end. Alexandria.Client contém o código do front-end, e Alexandria.Messages contém as definições de mensagem compartilhadas entre eles. Para executar o exemplo, Alexandria.Backend e Alexandria.Client devem estar em execução.

Uma vantagem de hospedar o back-end em um aplicativo de console é que você pode simular facilmente os cenários desconectados; basta encerrar o aplicativo de console de back-end e inicializá-lo depois.

Falácias da computação distribuída

De posse dos conceitos básicos de arquitetura, vamos dar uma olhada nas implicações de se criar um aplicativo cliente inteligente. A comunicação com o back-end será por meio de uma intranet ou da Internet. Considerando o fato de que a principal fonte de chamadas remotas na maioria dos aplicativos Web é um banco de dados ou outro servidor de aplicativos localizado no mesmo datacenter (e, muitas vezes, no mesmo rack), esta é uma mudança drástica que traz várias implicações.

As conexões de intranet e Internet têm problemas de velocidade, limitações de largura de banda e segurança. A grande diferença nos custos de comunicação ditam uma estrutura de comunicação diferente da que você adotaria se todas as principais partes do aplicativo residissem no mesmo datacenter.

Entre as maiores dificuldades com as quais você tem de lidar nos aplicativos distribuídos são as falácias da computação distribuída. Elas são um conjunto de suposições que os desenvolvedores tendem a fazer ao criar aplicativos distribuídos e que, no final das contas, se mostram falsas. Valer-se dessas falsas suposições geralmente resulta na diminuição de recursos ou em um custo muito alto para remodelar e recriar o sistema. Esta é uma lista de oito falácias:

  • A rede é confiável.
  • A latência é nula.
  • A largura de banda é infinita.
  • A rede é segura.
  • A topologia não muda.
  • Há um administrador.
  • O custo de transporte é zero.
  • A rede é homogênea.

Qualquer aplicativo distribuído que não levar em consideração essas falácias enfrentará sérios problemas. Um aplicativo cliente inteligente precisa lidar com elas tendo esses problemas em mente. O uso de cache é um tópico muito importante nessas circunstâncias. Mesmo que você não tenha interesse em trabalhar desconectado, quase sempre um cache é útil para aumentar a capacidade de resposta do aplicativo.

Outro aspecto que você precisa considerar é o modelo de comunicação do aplicativo. Pode parecer que o modelo mais simples é um proxy de serviço padrão que permite executar RPCs (chamadas de procedimento remoto), mas com o tempo isso tende a causar problemas. Ele demanda um código mais complexo para lidar com um estado desconectado e exige que você manipule chamadas assíncronas explicitamente se quiser impedir o bloqueio no thread de interface do usuário.

Conceitos básicos sobre back-end

Depois, existe o problema de como estruturar o back-end do aplicativo de uma forma que gere bom desempenho e um nível de separação em relação ao modo como a interface do usuário está estruturada.

O cenário ideal do ponto de vista do desempenho e da capacidade de resposta é fazer uma única chamada ao back-end para obter todos os dados necessários para a tela apresentada. O problema de seguir esta rota é que você termina com uma interface de serviço que imita exatamente a interface de usuário do cliente inteligente. Isso é ruim por uma série de motivos. O principal deles é que a interface do usuário é a parte mais mutável de um aplicativo. Vincular a interface de serviço à interface do usuário dessa maneira leva a alterações frequentes no serviço, em decorrência de alterações puramente na interface do usuário.

Por sua vez, isso significa que a implantação do aplicativo torna-se bem mais difícil. Você deve implantar tanto o front-end quanto o back-end simultaneamente, e tentar dar suporte a diversas versões ao mesmo tempo pode resultar em maior complexidade. Além disso, não é possível usar a interface de serviço para criar mais interfaces do usuário nem como ponto de integração para serviços adicionais ou de terceiros.

Se você tentar fazer o outro caminho, ou seja, criar uma interface refinada padrão, irá se deparar com as falácias (uma interface refinada leva a um número alto de chamadas remotas, o que resulta em problemas de latência, confiabilidade e largura de banda).

A resposta para este desafio é fugir do modelo RPC comum. Em vez de expor métodos para chamada remota, vamos usar um cache local e um modelo de comunicação orientado a mensagem.

A Figura 2 mostra como empacotar várias solicitações do front-end para o back-end. Isso permite que você faça uma única chamada remota, mas mantenha um modelo de programação no lado do servidor que não seja tão estritamente vinculado às necessidades da interface do usuário.

Figure 2 A Single Request to the Server Contains Several Messages

Figura 2 Uma única solicitação ao servidor contém várias mensagens

Para aumentar a capacidade de resposta, você pode incluir um cache local que responda a algumas consultas de imediato, o que torna um aplicativo mais responsivo.

Uma das coisas que devem ser consideradas nesses cenários é que tipos de dados você tem e os requisitos de atualização de todos os dados exibidos. No aplicativo Alexandria, dependo muito do cache local porque ele é aceitável para mostrar os dados de usuário em cache enquanto o aplicativo solicita dados atualizados do sistema. Outros aplicativos (comercialização de ações, por exemplo) provavelmente não devem mostrar nada além de dados antigos.

Operações desconectadas

O próximo problema que você precisa enfrentar é lidar com cenários desconectados. Em muitos aplicativos, é possível especificar que uma conexão seja obrigatória, o que significa que você pode simplesmente mostrar um erro para o usuário se os servidores back-end estiverem indisponíveis. Mas uma das vantagens de um aplicativo cliente inteligente é que ele pode funcionar desconectado, e o aplicativo Alexandria se beneficia totalmente disso.

No entanto, isso significa que o cache torna-se ainda mais importante porque ele é usado para agilizar a comunicação e fornecer dados armazenados em cache quando não é possível acessar o sistema back-end.

Creio que agora você já tem uma boa noção dos desafios da criação de um aplicativo como esse, então vamos para a próxima etapa, que é aprender a resolvê-los.

Filas são uma das coisas de que eu mais gosto

No Alexandria, não há comunicação RPC entre o front-end e o back-end. Em vez disso, conforme ilustrado na Figura 3, toda a comunicação é feita via mensagens unidirecionais que passam por filas.

Figure 3 The Alexandria Communication Model

Figura 3 O modelo de comunicação do Alexandria

As filas são uma forma muito elegante de resolver problemas de comunicação identificados anteriormente. Em vez de estabelecer comunicação direta entre o front-end e o back-end (o que significa que dar suporte a cenários desconectados é difícil), você pode deixar o subsistema de enfileiramento lidar com tudo isso.

Usar filas é muito simples. Você pede para o subsistema de enfileiramento local enviar uma mensagem para alguma fila. O subsistema de enfileiramento assume a propriedade da mensagem e assegura que ela chegue ao seu destino em algum momento. Todavia, o aplicativo não espera a mensagem chegar ao destino e pode continuar fazendo seu trabalho.

Se a fila de destino não estiver disponível, o subsistema de enfileiramento aguardará até que a fila fique novamente disponível e entregará a mensagem. Normalmente o subsistema de enfileiramento mantém a mensagem no disco até ela ser entregue, por isso as mensagens pendentes chegarão ao destino mesmo que o computador de origem tenha sido reiniciado.

Quando usamos filas, é muito fácil pensar em termos de mensagens e destinos. Uma mensagem recebida em um sistema back-end acionará alguma ação, que poderá resultar no envio de uma resposta para o remetente original. Observe que não há bloqueio em nenhum dos lados porque cada sistema é completamente independente.

Entre os subsistemas de enfileiramento estão MSMQ, ActiveMQ, RabbitMQ e outros. O aplicativo Alexandria usa o Rhino Queues (github.com/rhino-queues/rhino-queues), um subsistema de enfileiramento de código-fonte aberto implantado por xcopy. Escolhi o Rhino Queues simplesmente porque ele não exige instalação ou administração, o que o torna ideal para uso em amostras e nos aplicativos que você precisa para implantar em muitos computadores. Também vale a pena observar que eu sou o criador do Rhino Queues. Espero que você goste dele.

Colocando as filas para trabalhar

Vejamos como obter os dados para a tela principal usando filas. Aqui está a rotina de inicialização ApplicationModel:

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

Estou enviando um lote de mensagens para o servidor solicitando várias informações. Há várias coisas a serem observadas aqui. A granularidade das mensagens enviadas é alta. Em vez de enviar uma única mensagem geral, como MainWindowQuery, envio muitas mensagens (MyBooksQuery, MyQueueQuery e assim por diante), cada uma para uma informação específica. Conforme discutido anteriormente, isso permite que você aproveite a vantagem de enviar várias mensagens em um único lote (o que diminui as viagens de ida e volta na rede) e reduzir o acoplamento entre o front-end e o back-end.

O inimigo é o RPC

Um dos erros mais comuns ao criar um aplicativo distribuído é ignorar o aspecto da distribuição do aplicativo. O WCF, por exemplo, ajuda a ignorar o fato de que você não está fazendo uma chamada de método pela rede. Apesar de ser um modelo de programação muito simples, significa que você precisa ser extremamente cauteloso para não violar uma das falácias da computação distribuída.

Na verdade, é o fato de o modelo de programação oferecido por estruturas com o WCF ser tão parecido com o que você usa para chamar métodos no computador local que o leva a fazer essas suposições falsas.

Uma API de RPC padrão significa bloqueio ao fazer uma chamada pela rede, custo mais alto para cada chamada de método remota e o potencial de falha se o servidor back-end não estiver disponível. Certamente é possível criar um bom aplicativo distribuído com base nesse fundamento, mas isso demanda mais cuidado.

Seguir uma abordagem diferente leva a um modelo de programação baseado em trocas de mensagens explícitas (ao contrário das trocas de mensagens implícitas comuns na maioria das pilhas RPC baseadas em SOAP). A princípio esse modelo pode parecer estranho e de fato requer uma pequena mudança de pensamento, mas, ao fazer essa mudança, você diminui consideravelmente a complexidade de se preocupar com todos os aspectos.

Meu aplicativo de exemplo Alexandria foi desenvolvido com base em uma plataforma de mensagens unidirecionais e faz uso de toda esta plataforma, por isso sabe que é distribuído e, na verdade, se beneficia disso.

Observe que todas as mensagens terminam com o termo Query (consulta). Esta é uma convenção simples que uso para indicar mensagens de consulta puras que não mudam de estado e esperam algum tipo de resposta.

Por último, observe que parece que não estou recebendo nenhum tipo de resposta do servidor. Como estou usando filas, o modo de comunicação é “dispare e esqueça”. Eu disparo uma mensagem (ou um lote de mensagens) agora e lido com as respostas em uma etapa posterior.

Antes de saber como o front-end lida com as respostas, vejamos como o back-end processa as mensagens que acabei de enviar. A Figura 4 mostra como o servidor back-end consome uma consulta sobre livros. E aqui, pela primeira vez, você pode ver como eu uso o NHibernate e o Rhino Service Bus.

Figura 4 Consumindo uma consulta no 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()
    });
  }
}

Mas, antes de nos aprofundarmos no código que manipula essa mensagem, vamos falar sobre a estrutura em que ele é executado.

Estamos falando de mensagens

O Rhino Service Bus (hibernatingrhinos.com/open-source/rhino-service-bus) é, previsivelmente, uma implementação de barramento de serviço. Trata-se de uma estrutura de comunicação baseada em troca de mensagens enfileiradas unidirecionais, altamente inspirada pelo NServiceBus (nservicebus.com).

Uma mensagem enviada no barramento chegará à fila de destino, onde será chamado um consumidor de mensagem. Na Figura 4, esse consumidor é MyBooksQueryConsumer. Um consumidor de mensagem é uma classe que implementa ConsumerOf<TMsg>, e o método Consume é chamado com a instância de mensagem apropriada para manipular a mensagem.

Provavelmente você suspeite, com base no construtor MyBooksQueryConsumer, que estou usando um contêiner IoC (controle de inversão) para fornecer dependências para o consumidor de mensagem. No caso de MyBooksQueryConsumer, essas dependências são o próprio barramento e a sessão do NHibernate.

O código real para consumir a mensagem é simples. Você obtém o usuário apropriado da sessão do NHibernate e envia uma resposta de volta para o remetente da mensagem com os dados solicitados.

O front-end também tem um consumidor de mensagem. Este consumidor é 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);
  }
}

Ele simplesmente atualiza o modelo de aplicativo com os dados da mensagem. No entanto, devemos fazer uma observação: o método Consume não é chamado no thread de interface do usuário. Em vez disso, ele é chamado em um thread em segundo plano. Todavia, o modelo de aplicativo é vinculado à interface do usuário, por isso a sua atualização deve acontecer no thread de interface do usuário. O método UpdateFrom sabe disso e alternará para o thread da IU para atualizar o modelo de aplicativo no thread correto.

O código para manipular as outras mensagens no back-end e no front-end é semelhante. Esta comunicação é puramente assíncrona. Em nenhum momento você fica esperando uma resposta do back-end e você também não usa a API assíncrona do .NET Framework. Pelo contrário, você tem uma troca de mensagens explícita, que normalmente acontece quase que de forma instantânea, mas também pode se estender por um longo período caso você esteja trabalhando no modo desconectado.

Antes, quando enviava consultas para o back-end, apenas dizia ao barramento para enviar as mensagens, mas não falava para onde elas deveriam ser enviadas. Na Figura 4, chamei Reply, novamente não especificando para onde a mensagem deveria ser enviada. Como o barramento sabe para onde enviar essas mensagens?

No caso de enviar mensagens para o back-end, a resposta é: configuração. Em App.config, você encontrará a seguinte configuração:

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

Isso diz para o barramento que todas as mensagens cujo namespace começa com Alexandria.Messages devem ser enviadas ao ponto de extremidade alexandria_backend.

Durante o manuseio das mensagens no sistema back-end, chamar Reply simplesmente significa enviar a mensagem de volta para o remetente.

Essa configuração especifica a propriedade da mensagem, ou seja, para quem ela deve ser enviada quando colocada no barramento e para onde enviar uma solicitação de assinatura de modo que você seja incluído na lista de distribuição quando mensagens desse tipo são publicadas. Não estou usando publicação de mensagem no aplicativo Alexandria, por isso não abordarei esse tema.

Gerenciamento de sessões

Você viu como o mecanismo de comunicação funciona, mas existem questões de infraestrutura que devem ser resolvidas antes de irmos adiante. Como em qualquer aplicativo NHibernate, você precisa de alguma forma de gerenciar o ciclo de vida de sessões e lidar com as transações corretamente.

A abordagem padrão para aplicativos Web é criar uma sessão por solicitação, de modo que cada solicitação tenha sua própria sessão. No caso de um aplicativo de mensagens, o comportamento é quase idêntico. Em vez de ter uma sessão por solicitação, você tem uma sessão por lote de mensagens.

Como consequência, a infraestrutura cuida disso quase que completamente. A Figura 5 mostra o código de inicialização para o sistema back-end.

Figura 5 Inicializando sessões de mensagens

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();
  }
}

Inicialização é um conceito explícito no Rhino Service Bus, implementado por classes derivadas de AbstractBootStrapper. O inicializador tem o mesmo trabalho que Global.asax em um aplicativo Web típico. Na Figura 5, primeiro criei o alocador de sessão do NHibernate e configurei o contêiner (Castle Windsor) para fornecer a sessão do NHibernate de NHibenrateMessageModule.

Um módulo de mensagem tem a mesma finalidade de um módulo HTTP em um aplicativo Web: lidar com questões abrangentes entre todas as solicitações. Eu uso NHibernateMessageModule para gerenciar a duração de uma sessão, conforme ilustrado na Figura 6.

Figura 6 Gerenciando a duração de uma sessão

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;
  }
}

O código é muito simples: registrar-se para os eventos apropriados, criar e descartar a sessão nos lugares apropriados e pronto.

Uma implicação interessante dessa abordagem é que todas as mensagens de um lote compartilharão a mesma sessão, o que significa que, em muitos casos, você poderá se beneficiar do cache de primeiro nível do NHibernate.

Gerenciamento de transações

Isso é tudo sobre o gerenciamento de sessões, mas e quanto a transações?

Uma prática recomendada para o NHibernate é que todas as interações com o banco de dados devem ser gerenciadas por meio de transações. Mas não vou usar transações do NHibernate aqui. Por quê?

A resposta é porque as transações são gerenciadas pelo Rhino Service Bus. Em vez de fazer com que cada consumidor gerencie suas próprias transações, o Rhino Service Bus adota outra abordagem. Ele utiliza System.Transactions.TransactionScope para criar uma única transação que abranja todos os consumidores de mensagens do lote.

Isso significa que todas as ações executadas em resposta a um lote de mensagens (e não a uma única mensagem) façam parte da mesma transação. O NHibernate automaticamente relacionará uma sessão na transação de ambiente para que, quando você estiver usando o Rhino Service Bus, não precise lidar explicitamente com transações.

A combinação de uma única sessão e uma única transação facilita combinar várias operações em uma única unidade transacional. Isso também significa que você pode se beneficiar diretamente do cache de primeiro nível do NHibernate. Por exemplo, este é o código relevante para manipular 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()
  });
}

O código propriamente dito para manipular MyQueueQuery e MyBooksQuery é praticamente idêntico. Então qual é a implicação de desempenho de uma única transação por sessão do seguinte código?

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

À primeira vista, parece que seria preciso quatro consultas para coletar todas as informações necessárias. Em MyBookQuery, uma consulta para obter o usuário apropriado e outra para carregar os livros do usuário. O mesmo parece válido para MyQueueQuery: uma consulta para obter o usuário apropriado e outra para carregar a fila dele.

No entanto, o uso de uma única sessão para o lote inteiro mostra que, na verdade, você está utilizando o cache de primeiro nível para evitar consultas desnecessárias, como podemos ver na saída do NHibernate Profiler (nhprof.com) mostrada na Figura 7.

image: The NHibnerate Profiler View of Processing Requests
Figura 7 Exibição do NHibnerate Profiler de solicitações de processamento

Suporte para cenários ocasionalmente conectados

Da forma como está, o aplicativo não mostrará um erro se não for possível se conectar com o servidor back-end, mas isso também não seria muito útil.

A próxima etapa na evolução desse aplicativo é transformá-lo em um verdadeiro cliente ocasionalmente conectado introduzindo um cache que permita que ele continue funcionando mesmo que o servidor back-end não esteja respondendo. Mas não usarei a arquitetura de cache tradicional, em que o código do aplicativo faz chamadas explícitas ao cache. Em vez disso, vou aplicar o cache no nível de infraestrutura.

A Figura 8 mostra a sequência de operações quando o cache é implementado como parte da infraestrutura de mensagens quando você envia uma única mensagem solicitando dados sobre os livros de um usuário.

image: Using the Cache in Concurrent Messaging Operations

Figura 8 Usando o cache em operações de mensagens simultâneas

O cliente envia uma mensagem MyBooksQuery. A mensagem é enviada no barramento e, ao mesmo tempo, o cache é consultado para saber se ele tem a resposta para esta solicitação. Se o cache contém a resposta para a solicitação anterior, imediatamente ele faz com que a mensagem armazenada seja consumida como se tivesse acabado de chegar ao barramento.

A resposta do sistema back-end chega. Ela é consumida normalmente e também colocada no cache. Superficialmente, essa abordagem parece complicada, mas ela resulta em um comportamento de armazenamento em cache eficaz e permite ignorar quase que completamente as questões de cache no código do aplicativo. Com um cache persistente (que sobrevive a reinicializações do aplicativo), você pode operar o aplicativo de uma forma completamente independente, sem precisar de nenhum dado do servidor back-end.

Agora vamos implementar essa funcionalidade. Eu aceito um cache persistente (o código de exemplo propicia uma implementação simples que usa a serialização binária para salvar os valores em disco) e defino as seguintes convenções:

  • É possível armazenar uma mensagem em cache se ela faz parte de uma troca de mensagens de solicitação/resposta.
  • Tanto as mensagens de solicitação quanto as de resposta carregam a chave do cache para a troca de mensagens.

A troca de mensagens é definida por uma interface ICacheableQuery com uma única propriedade Key e por uma interface ICacheableResponse com propriedades Key e Timestamp.

Para implementar esta convenção, crio um CachingMessageModule que será executado no front-end, interceptando mensagens de entrada e saída. A Figura 9 mostra como as mensagens de entrada são manipuladas.

Figura 9 Cache de conexões de entrada

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;
}

Não há nada de especial aqui: se a mensagem é uma resposta armazenável em cache, eu a coloco no cache. Mas preciso fazer uma observação: eu lido com mensagens fora de ordem — mensagens que têm um carimbo de data/hora anterior chegando após mensagens com carimbos de data/hora mais recentes. Isso assegura que apenas as informações mais recentes são armazenadas no cache.

Manipular mensagens de saída e despachar as mensagens do cache é mais interessante, como você pode ver na Figura 10.

Figura 10 Despachando mensagens

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);
}

Eu coleto as respostas armazenadas do cache e chamo ConsumeMessages nelas. Isso faz com que o barramento chame a lógica de chamada de mensagens usual, de modo que pareça que a mensagem chegou novamente.

No entanto, observe que, embora haja uma resposta em cache, você ainda envia a mensagem. O raciocínio é que você pode dar uma resposta rápida (em cache) para o usuário e atualizar as informações mostradas para ele quando o back-end responde a novas mensagens.

Próximas etapas

Eu abordei os elementos básicos de um aplicativo cliente inteligente: como estruturar o back-end e o modo de comunicação entre o aplicativo cliente inteligente e o back-end. Esse último aspecto é importante porque escolher o modo de comunicação errado pode levar às falácias da computação distribuída. Também falei sobre envio em lote e armazenamento em cache, duas abordagens muito importantes para melhorar o desempenho de um aplicativo cliente inteligente.

No back-end, você viu como gerenciar transações e a sessão do NHibernate, como consumir e responder a mensagens do cliente e como tudo se combina no inicializador.

Neste artigo, me concentrei principalmente em questões de infraestrutura; no próximo artigo, falarei sobre práticas recomendadas de envio de dados entre o back-end e o aplicativo cliente inteligente e sobre os padrões de gerenciamento de alterações distribuídas.

Oren Eini (pseudônimo Ayende Rahien) é membro ativo de vários projetos de código-fonte aberto (entre eles, NHibernate e Castle) e fundador de muitos outros (como Rhino Mocks, NHibernate Query Analyzer e Rhino Commons). Eini também é responsável pelo NHibernate Profiler (nhprof.com), um depurador visual para NHibernate. Você pode acompanhar o trabalho de Eini em ayende.com/blog.