Desenvolvimento - C#
WCF - Message Queue
Para garantir a entrega da mensagem e o processamento assíncrono da operação (mesmo quando o serviço estiver offline), o WCF faz uso do Microsoft Message Queue. Este artigo irá explorar as funcionalidades e, principalmente, os benefícios fornecidos por essa integração.
por Israel AéceO Microsoft Message Queue é uma tecnologia que está ligada diretamente ao sistema operacional que, entre suas diversas funcionalidades, temos a durabilidade, suporte à transações, garantia de entrega, etc. O Message Queue é um componente adicional, e que pode ser instalado a partir dos recursos do Windows. O Windows XP e 2003 trazem a versão 3.0 do Message Queue, enquanto o Windows Vista e 2008 disponibilizam a versão 4.0.
A disponibilidade é uma das principais características de uma aplicação que a faz utilizar o Message Queue. O fato do serviço não estar online nem sempre é um problema; é perfeitamente possível que algum cliente seja um dispositivo móvel, fazendo com que o serviço esteja inalcançável e, com isso, em um ambiente tradicional, qualquer chamada para alguma operação iria falhar. Com a durabilidade, o Message Queue garante que a mensagem seja persistida fisicamente e, quando a conexão for restabelecida, a mesma será enviada para o serviço.
Podemos interagir com o Message Queue de duas formas: a primeira é utilizando a console de gerenciamento que é criada quando você instala o Message Queue; já a segunda é através do .NET, que fornece um Assembly chamado System.Messaging.dll com vários tipos para criar, enviar e remover mensagem de uma fila. O namespace System.Messaging existe desde a versão 1.0 do .NET Framework, mas o WCF encapsula o uso dele e, felizmente, não precisaremos recorrer a qualquer classe deste namespace para fazer com que o Message Queue funcione em conjunto com o WCF.
Filas Públicas e Privadas
O Message Queue possibilita a criação de dois tipos de filas, a saber: públicas e privadas. As filas públicas obrigatoriamente devem estar registradas em um domínio, através do Active Directory, podendo ser acessadas por todas as máquinas que estão sob aquele domínio. Já as filas privadas tem um escopo bem mais restrito, ou seja, podem ser acessadas apenas dentro da máquina onde elas foram criadas.
Durante a criação de uma fila, via console de gerenciamento ou através da API System.Messaging, podemos definir se a mesma será ou não transacionada. Marcando a fila como transacionada, tanto a inserção de uma nova mensagem com a remoção de uma mensagem existente será protegida por uma transação. Quando falamos especificamente sobre transações no WCF com o Message Queue, há alguns detalhes que temos que nos atentar e que veremos mais tarde, ainda neste mesmo artigo.
Chamadas Enfileiradas e Processamento Assíncrono
Ao invocar uma operação onde temos o Message Queue envolvido, ele trará vários benefícios. Em um formato tradicional, utilizando outros protocolos mais convencionais, como o HTTP, TCP ou IPC, demanda que o serviço esteja disponível para que a mensagem chegue até ele e seja processada e, caso contrário, uma exceção será disparada no cliente.
Com a integração do Message Queue, o WCF persistirá a mensagem localmente em uma fila caso o serviço não esteja disponível. Ao persistir a mensagem, a garantia de entrega será assegurada pelo Message Queue de forma transparente para a aplicação cliente e, quando o serviço ficar novamente ativo, a mensagem será encaminhada para o serviço para efetuar o processamento da(s) operação(ões) (algo que já era suportado no COM+). É importante dizer que não há uma relação entre chamada à uma operação e uma mensagem na fila do Message Queue; poderá haver mensagens que acomodarão mais que uma operação (falaremos detalhadamente sobre isso mais tarde, ainda neste artigo) A imagem abaixo ilustra superficialmente como as mensagens são enviadas/recebidas quando o serviço é exposto via Message Queue:
Figura 1 - Serviço exposto via Message Queue. |
Como a fila em que as operações serão persistidas estará sempre disponível, o WCF sempre armazenará localmente e, com isso, a aplicação poderá continuar trabalhando sem esperar que a mensagem seja entregue, garantindo assim o que chamamos de processamento assíncrono. Pelo fato das mensagens estarem persistidas, elas conseguirão sobreviver a possíveis reinicializações do cliente e, quando o mesmo retornar, novas tentativas serão realizadas até que a mensagem seja efetivamente entregue ao destino. Obviamente que se o cliente e o serviço estiverem online, a mensagem será entregue imediatamente.
MSMQ e o WCF
Quando formos desenhar um contrato para ser exposto através do Message Queue, um cuidado que devemos ter é com relação ao tipo da operação. No tópico anterior falamos sobre as necessidades da utilização do Message Queue e, analisando essas características, vemos que as operações que serão expostas através do Message Queue não devem retornar nenhum resultado, e também possíveis exceções nunca chegarão até o cliente, já o WCF desabilita os contratos de faults em operações enfileiradas. A finalidade do contrato é apenas definir a semântica da aplicação e, durante a execução, a chamada poderá ou não ser persistida e mais tarde processada.
Com isso, o WCF nos obriga a definir todas as operações de um contrato que serão expostas através do Message Queue como sendo one-way (mais detalhes neste artigo). Caso você exponha uma das operações sem antes definí-la como one-way, uma exceção do tipo InvalidOperationException será disparada antes da abertura do host. Com exceção deste detalhe, não há nada diferente a ser realizado em relação à implementação ou chamadas às operações enfileiradas. Note que o código abaixo exibe a criação deste contrato:
using System; using System.ServiceModel; [ServiceContract] [DeliveryRequirements(QueuedDeliveryRequirements = QueuedDeliveryRequirementsMode.Required)] public interface IContrato { [OperationContract(IsOneWay = true)] void EnviarDados(string msg); } Imports System Imports System.ServiceModel <ServiceContract(), _ DeliveryRequirements(QueuedDeliveryRequirements:=QueuedDeliveryRequirementsMode.Required)> _ Public Interface IContrato <OperationContract(IsOneWay:=True)> Sub EnviarDados(ByVal msg As String) End Interface |
|||
C# | VB.NET |
O WCF também permite a você especificar no contrato que o mesmo deverá ser exposto sob um binding que suporte chamadas enfileiradas. Para isso, basta recorrermos ao atributo DeliveryRequirementsAttribute, definindo a propriedade QueuedDeliveryRequirements com uma das três opções definidas no enumerador QueuedDeliveryRequirementsMode:
Allowed: O binding pode ou não suportar chamadas enfileiradas.
Required: O binding deve suportar chamadas enfileiradas.
NotAllowed: O binding não deve suportar chamadas enfileiradas.
Esse atributo ainda fornece duas outras propriedades: RequireOrderedDelivery e TargetContract. A primeira propriedade determina se o binding deverá ou não garantir a entrega ordenada das mensagens. Já a segunda propriedade espera um objeto do tipo Type, que determina em qual contrato essa técnica será aplicada. Essa propriedade somente faz sentido quando o atributo é aplicado na classe que representa o serviço, ao invés do contrato.
Hosting e Binding
Como já sabemos, o binding contém os aspectos de comunicação, especificando o meio de transporte, codificação, etc. Para expor um serviço via Message Queue, devemos recorrer a um binding exclusivo para isso, o NetMsmqBinding. Apesar de não expor todas as propriedades suportadas pelo Message Queue, este binding traz as principais funcionalidades necessárias para a utilização do mesmo através do WCF.
Para especificar o endereço onde o serviço será exposto, devemos utilizar a seguinte convenção (note que não há o caracter $): net.msmq://NomeDaMaquina/Private/NomeDaFila. Como o WCF não pode publicar o documento WSDL através do Message Queue, é necessária a criação de um endpoint exclusivo para a publicação do mesmo, através de algum outro protocolo, como o HTTP ou TCP. Caso isso não seja feito, os clientes não conseguirão referenciar o serviço e criar o proxy. O trecho de código abaixo ilustra como devemos proceder para configurar o host:
using System; using System.ServiceModel; using System.ServiceModel.Description; using (ServiceHost host = new ServiceHost(typeof(Servico), new Uri[] { new Uri("net.msmq://localhost/private/FilaDeTestes"), new Uri("http://localhost:8383/") })) { host.Description.Behaviors.Add(new ServiceMetadataBehavior()); host.AddServiceEndpoint( typeof(IContrato), new NetMsmqBinding(NetMsmqSecurityMode.None), string.Empty); host.AddServiceEndpoint( typeof(IMetadataExchange), MetadataExchangeBindings.CreateMexHttpBinding(), "mex"); host.Open(); Console.ReadLine(); } Imports System Imports System.ServiceModel Imports System.ServiceModel.Description Using host As New ServiceHost(GetType(Servico), New Uri() { _ New Uri("net.msmq://localhost/private/FilaDeTestes"), _ New Uri("http://localhost:8383/")}) host.Description.Behaviors.Add(New ServiceMetadataBehavior()) host.AddServiceEndpoint( _ GetType(IContrato), _ New NetMsmqBinding(NetMsmqSecurityMode.None), _ String.Empty) host.AddServiceEndpoint( _ GetType(IMetadataExchange), _ MetadataExchangeBindings.CreateMexHttpBinding(), _ "mex") host.Open() Console.ReadLine() Using |
|||
C# | VB.NET |
A configuração do host não tem muitas diferenças em relação a um serviço exposto via qualquer outro tipo de binding. Estamos utilizando o NetMsmqBinding com sua configuração padrão, ou seja, nenhuma das propriedades expostas por ele foi customizada. Como falamos acima, este binding traz várias propriedades que podem ser configuradas (de forma imperativa ou declarativa) para customizar o envio/processamento das mensagens. A tabela abaixo lista essas propriedades e suas respectivas descrições:
|
Dentre as propriedades acima, algumas se referem a dois tipos especiais de filas: dead-letter queue e poison message queue. Esses tipos especiais, criados pelo sistema, são extremamente importantes para garantir o funcionamento de algumas configurações expostas pelo Message Queue. Em outras palavras, as dead-letter queues lidam com problemas relativos à comunicação e as poison message queues se limitam a tratar problemas que ocorrem dentro da execução da operação.
Há vários problemas que podem acontecer durante a tentativa de entrega da mensagem; entre esses problemas temos falhas na infraestrutura, a fila foi excluída, falha na autenticação, etc. Esses tipos de problemas fazem com que a mensagem seja enviada para uma fila especial, chamada de dead-letter queue, ficando a mensagem ali até o momento em que uma outra aplicação ou o administrador do sistema tome alguma decisão (atente-se a expiração que a mensagem poderá ter). O Windows já disponibiliza dois tipos de dead-letter queue: Dead-letter messages para mensagens não transacionadas e Transactional dead-letter messages para mensagens transacionadas.
Como as filas que mencionamos acima são fornecidas pelo próprio sistema operacional, elas são compartilhadas entre todas as aplicações que rodam naquela máquina. Apesar da fila aceitar as mensagens independente de onde elas vieram, ficará difícil a manutenção nelas. Muitas aplicações já fornecem o suporte para processamento das mensagens nesta fila, mas como distinguir qual mensagem pertence àquela aplicação? Visando sanar este problema é que recorremos à criação de uma fila customizada (através das propriedades DeadLetterQueue e CustomDeadLetterQueue) para catalogar as mensagens problemáticas, fornecendo um isolamento entre as aplicações.
Quando optamos por criar uma fila customizada para servir como dead-letter queue, esta é como uma fila normal, não havendo nada de especial, mas atentando-se à definí-la ou não como transacional, dependendo da sua necessidade. A configuração do binding muda ligeiramente, definindo agora a fila customizada como dead-letter queue, assim como é mostrado no código:
using System; using System.ServiceModel; using System.ServiceModel.Description; using (ServiceHost host = new ServiceHost(typeof(Servico), new Uri[] { new Uri("net.msmq://localhost/private/FilaDeTestes"), new Uri("http://localhost:8383/") })) { host.Description.Behaviors.Add(new ServiceMetadataBehavior()); NetMsmqBinding binding = new NetMsmqBinding(NetMsmqSecurityMode.None); binding.DeadLetterQueue = DeadLetterQueue.Custom; binding.CustomDeadLetterQueue = new Uri("net.msmq://localhost/private/MensagensProblematicas"); host.AddServiceEndpoint( typeof(IContrato), binding, string.Empty); host.AddServiceEndpoint( typeof(IMetadataExchange), MetadataExchangeBindings.CreateMexHttpBinding(), "mex"); host.Open(); Console.ReadLine(); } Imports System Imports System.ServiceModel Imports System.ServiceModel.Description Using host As New ServiceHost(GetType(Servico), New Uri() { _ New Uri("net.msmq://localhost/private/FilaDeTestes"), _ New Uri("http://localhost:8383/")}) host.Description.Behaviors.Add(New ServiceMetadataBehavior()) Dim binding As New NetMsmqBinding(NetMsmqSecurityMode.None) binding.DeadLetterQueue = DeadLetterQueue.Custom binding.CustomDeadLetterQueue = _ New Uri("net.msmq://localhost/private/MensagensProblematicas") host.AddServiceEndpoint( _ GetType(IContrato), _ binding, _ String.Empty) host.AddServiceEndpoint( _ GetType(IMetadataExchange), _ MetadataExchangeBindings.CreateMexHttpBinding(), _ "mex") host.Open() Console.ReadLine() Using |
|||
C# | VB.NET |
Observações: Lembre-se de que a fila criada para servir como dead-letter queue é uma fila normal e, para processar as mensagens que estão dentro dela, basta criar um novo serviço que extraia as mensagens e efetue o devido processamento. Se por algum motivo você queira acessar as filas de dead-letter do sistema, você poderá acessá-la partir dos seguintes endereços: net.msmq://localhost/system$;DeadLetter (Dead-letter messages) e net.msmq://localhost/system$;DeadXact (Transactional dead-letter messages).
Ainda falando sobre filas especiais, temos a poison queue. Há mensagens que podem falhar durante o processamento por vários motivos. Por exemplo, ao processar uma mensagem e salvar algumas informações em um banco de dados, algum problema pode acontecer, como é o caso de um deadlock, fazendo com que a transação seja abortada e a mensagem seja devolvida para a fila. Isso fará com que a mensagem seja novamente reprocessada e dependendo do problema que está acontecendo e não havendo estratégia para remover a mensagem da fila, poderemos ter um loop infinito.
Para evitar que problemas como este ocorram, o Message Queue possui algumas configurações que permitem determinar a quantidade de tentativas e, quando elas se esgotarem, a mensagem é enviada para uma fila do tipo poison. Para lidar com esta técnica, o Message Queue cria duas "sub-filas" abaixo da fila principal, chamadas de retry e poison. A primeira "sub-fila" armazenará as mensagens que estão em uma de suas tentativas de processamento; já a segunda "sub-fila", poison, armazenará as mensagens que não foram processadas com sucesso, mesmo depois de todas as tentativas, evitando assim que o loop infinito não aconteça. O exemplo abaixo ilustra como configurar o binding para suportar essa técnica:
using System; using System.ServiceModel; using System.ServiceModel.Description; using (ServiceHost host = new ServiceHost(typeof(Servico), new Uri[] { new Uri("net.msmq://localhost/private/FilaDeTestes"), new Uri("http://localhost:8383/") })) { host.Description.Behaviors.Add(new ServiceMetadataBehavior()); NetMsmqBinding binding = new NetMsmqBinding(NetMsmqSecurityMode.None); binding.DeadLetterQueue = DeadLetterQueue.Custom; binding.CustomDeadLetterQueue = new Uri("net.msmq://localhost/private/MensagensProblematicas"); binding.MaxRetryCycles = 2; binding.ReceiveRetryCount = 2; binding.RetryCycleDelay = TimeSpan.FromSeconds(10); binding.ReceiveErrorHandling = ReceiveErrorHandling.Move; host.AddServiceEndpoint( typeof(IContrato), binding, string.Empty); host.AddServiceEndpoint( typeof(IMetadataExchange), MetadataExchangeBindings.CreateMexHttpBinding(), "mex"); host.Open(); Console.ReadLine(); } Imports System Imports System.ServiceModel Imports System.ServiceModel.Description Using host As New ServiceHost(GetType(Servico), New Uri() { _ New Uri("net.msmq://localhost/private/FilaDeTestes"), _ New Uri("http://localhost:8383/")}) host.Description.Behaviors.Add(New ServiceMetadataBehavior()) Dim binding As New NetMsmqBinding(NetMsmqSecurityMode.None) binding.DeadLetterQueue = DeadLetterQueue.Custom binding.CustomDeadLetterQueue = _ New Uri("net.msmq://localhost/private/MensagensProblematicas") binding.MaxRetryCycles = 2 binding.ReceiveRetryCount = 2 binding.RetryCycleDelay = TimeSpan.FromSeconds(10) binding.ReceiveErrorHandling = ReceiveErrorHandling.Move host.AddServiceEndpoint( _ GetType(IContrato), _ binding, _ String.Empty) host.AddServiceEndpoint( _ GetType(IMetadataExchange), _ MetadataExchangeBindings.CreateMexHttpBinding(), _ "mex") host.Open() Console.ReadLine() Using |
|||
C# | VB.NET |
Observação: Uma vez que as mensagens são movidas para uma fila do tipo poison, elas somente poderão ser acessadas acrescentando a palavra poison no final do nome da fila, separando por um ";", assim como é mostrado a seguir: net.msmq://localhost/private/FilaDeTestes;poison.
Ainda há a possibilidade de efetuar o hosting de um serviço que utiliza o Message Queue utilizando o WPAS (Windows Process Activation Service), fazendo com que o serviço seja exposto através do IIS, tirando proveito de todos os benefícios fornecidos por ele. Essa opção está desabilitada e é necessário instalar este recurso explicitamente a partir do Windows. Isso fará com que um novo serviço, chamado Net.Msmq Listener Adapter, seja instalado e deverá estar funcionando para permitir o serviço.
Transações
O Message Queue também é considerado um resource manager transacional. Isso quer dizer que tanto a entrada quanto a extração de uma mensagem na fila poderá ser envolvida através de uma transação. Como já falado anteriormente, isso somente será possível se durante a criação da fila você especifique que a mesma seja uma fila transacional.
Quando estamos falando de uma fila transacional, temos alguns detalhes e técnicas que podemos fazer uso para tirar o melhor proveito das transações. Antes de mais nada, precisamos entender como as transações estão distribuídas durante o processo de criação, entrega e processamento da mensagem. Cada uma destas etapas exige uma transação e, para ter uma visão mais detalhada, vamos analisar a mesma imagem que vimos acima só que exibindo onde estão essas possíveis transações:
Figura 2 - Transações que envolvem Message Queue quando exposto via WCF. |
1 - Client Transaction: Caso a chamada para a operação esteja envolvida em uma transação, a inserção da mensagem no Message Queue também será protegida por esta mesma transação. Depois da mensagem persistida no Message Queue e, se por algum motivo, a transação for abortada, automaticamente a mensagem será descartada. Felizmente a tentativa de entrega não acontecerá até que a transação seja "comitada".
2 - Delivery Transaction: A transação neste caso protegerá a entrega da mensagem entre o cliente e o servidor (obviamente quando a fila for transacional). Se a entrega falhar por qualquer razão, a mensagem será seguramente devolvida para o cliente e, mais tarde, o Message Queue efetuará uma nova tentativa.
3 - Playback Transaction: Uma vez que a mensagem foi entregue com sucesso para o servidor, entra em cena uma nova transação, chamada de playback transaction. A finalidade desta transação é proteger a mensagem durante o processamento da mesma. Se qualquer problema acontecer durante a execução da operação, a mensagem será devolvida para a fila, valendo a partir daqui o mecanismo de tentativas automáticas, que vimos anteriormente.
Uma questão que aparece quando isso é apresentado é como fazer parte da transação já criada pela própria plataforma ou como criar uma nova transação. Com exceção do processo 2 (Delivery Transaction), podemos criar códigos para fazer parte da transação que coloca a mensagem na fila quanto remover a mensagem dela. Para que isso seja possível não há muito segredo, bastando apenas recorrer à alguns tipos fornecidos pelo próprio WCF como pelo namespace System.Transactions e que já foram detalhadamente falados neste artigo.
Se quisermos criar um código que faça parte da mesma transação que coloca a mensagem na fila do lado do cliente (passo 1), basta instanciar a classe TransactionScope e envolver a chamada da operação dentro deste escopo transacional. Já no passo 3, para que o processamento da operação faça parte da mesma transação que é usada para extrair a mensagem, basta definirmos para True a propriedade TransactionScopeRequired do atributo OperationBehaviorAttribute que, por definição, se existir uma transação em aberto, a operação fará parte da mesma. Finalmente, se quisermos que a operação execute dentro de uma nova transação, basta criarmos um escopo transacionado através da classe TransactionScope e, em seu construtor, especificamos a opção RequiresNew, fornecida pelo enumerador TransactionScopeOption.
Gerenciamento de Instâncias
A escolha do modo de gerenciamento de instâncias do serviço implicará durante a execução do processamento das operações. Quando o serviço é exposto através do modo PerCall, cada chamada a qualquer operação será criada uma nova mensagem dentro da fila. Já no modo PerSession, se a sessão for requerida, as operações invocadas a partir de uma instância do proxy serão agrupadas em uma única mensagem. Finalmente, como o modo Single não pode definir uma sessão, cada chamada será sempre mapeada para uma mensagem dentro da fila.
Quando o host (ServiceHost) é criado para expor um serviço sob o Message Queue, o WCF cria de forma transparente um listener chamado MSMQ Channel Listener e tem um papel extremamente importante durante o processamento das mensagens. Como sabemos, cada modo de gerenciamento determina a criação da instância da classe que representa o serviço para atender às requisições. Este listener é responsável por extrair as mensagens da fila, criar a instância da classe e encaminhar as chamadas que estão na mensagem do Message Queue e encaminhá-las para sua respectiva instância.
Conclusão: Como pudemos ver no decorrer deste artigo, o WCF permite uma forte integração com o Message Queue para enriquecer ainda mais as características de um serviço, incrementado-o com a garantia de entrega da mensagem e ordenação, durabilidade, processamento assíncrono, chamadas enfileiradas e podendo tudo isso ser envolvido por transações para assegurar a consistência do processo. E, por fim, o uso do Message Queue permitirá aos clientes continuarem seu trabalho, mesmo quando o serviço não esteja acessível.