LINQ To SQL e Processamento Assíncrono do ASP.NET

escrevi e palestrei sobre a vantagem que temos ao fazer uso das páginas assíncronas, recurso que é fornecido a partir do ASP.NET 2.0.

Infelizmente o LINQ To SQL não possui intrinsicamente métodos para executar as queries de forma assíncrona e, sendo assim, não podemos incorporá-lo na execução assíncrona da página ASP.NET. Vale lembrar que voce pode criar um delegate apontando para um método, e dentro deste invocar as queries a partir do contexto do LINQ To SQL.

Utilizando esta técnica não trará os benefícios propostos pelas páginas assíncronas, pois quando voce invocar o delegate, ele extrairá uma thread do ThreadPool para executar a tarefa. A idéia das páginas assíncronas é fazer com que o processo custoso, como acesso a banco de dados, web services, etc., seja disparado através de uma thread de IO, liberando as threads do ThreadPool apenas para executar as páginas ASP.NET.

Para fazer o LINQ To SQL (ou qualquer outra tarefa) executar em uma thread de IO, podemos recorrer ao Power Threading, criada pela Wintellect. Essa library possui várias classes que nos auxiliam em tarefas assíncronas e, entre elas, temos a classe chamada CallbackThreadPool, que encapsula e gerencia a execução de tarefas a partir de threads de IO. Um único detalhe que precisamos nos atentar é a criação de um IAsyncResult customizado, que será utilizado pelo ASP.NET para determinar quando a query executada pelo LINQ To SQL for finalizada.

A parte mais complexa é a criação do IAsyncResult. Além da sua principal finalidade, ele também trará o resultado do processo assíncrono e possíveis exceções que essa tarefa possa disparar. Para facilitar e também conseguir reutilizar essa classe por várias entidades, eu a criei de forma genérica, trabalhando fortemente tipada. Para poupar espaço, abaixo consta apenas a sua definição.

public class DataAsyncResult<TResult> : IAsyncResult
{
    //implementação
}

A classe CallbackThreadPool fornece um método QueueUserWorkItem, e recebe como parametro uma instancia do delegate WaitCallback e da classe DataAsyncResult. O delegate deverá apontar para o método que irá executar a query via LINQ To SQL. O código abaixo ilustra como proceder para alistar um novo processo assíncrono através da Power Threading da Wintellect:

DataAsyncResult<IEnumerable<Cliente>> ar = new DataAsyncResult<IEnumerable<Cliente>>(callback, state);
_threadPool.QueueUserWorkItem(new WaitCallback(RecuperarDados), ar);

A partir deste ponto tudo é como já acontece normalmente com uma página assíncrona, ou seja, definindo o atributo Async da diretiva @Page como True e registrar a execução do processo assíncrono através do método AddOnPreRenderCompleteAsync da classe Page. Código de exemplo:

AsyncLINQToSQL.zip (39.97 kb)

WCF – MessageQueue

Ao efetuar uma chamada para uma operação de um determinado serviço, desejamos que ela seja sempre executada. Mas nem sempre há como garantir isso, já que o serviço que atende as requisições, por algum motivo, está indisponível naquele momento. Isso fará com que as requisições sejam rejeitadas e o cliente somente conseguirá executá-la quando o serviço estiver novamente no ar. 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.

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

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

Propriedade Descrição
CustomDeadLetterQueue Recebe uma instância da classe Uri (com o formato “net.msmq”) representando uma dead-letter queue customizada a ser utilizada pela aplicação. Essa propriedade trabalhará em conjunto com a propriedade DeadLetterQueue.
DeadLetterQueue Esta propriedade irá especificar o tipo da dead-letter queue. O tipo poderá ser definido com uma das três opções fornecidas pelo enumerador DeadLetterQueue:

  • Nome: A dead-letter queue não é requerida. Se a entrega da mensagem falhar, ela não será gravada em lugar algum. Essa opção é utilizada quando a propriedade ExactlyOnce é definida como False.

  • System: Determina que a dead-letter queue do sistema será utilizada para armazenar as mensagens que falharem. Há dead-letter queue transacional e não transacional. Quando a propriedade ExactlyOnce estiver definida como True, essa opção será utilizada.

  • Custom: Nesta opção você pode especificar uma fila própria para ser a sua dead-letter queue. Esta opção trabalhará em conjunto com a propriedade CustomDeadLetterQueue.

Durable Propriedade do tipo booleana que quando definida como True (padrão), o binding irá garantir a durabilidade da mensagem persistindo-a no disco. Quando definida como False, a mensagem será armazenada de forma “volátil”, ou seja, ela não conseguirá sobreviver a possíveis reinicializações do serviço do Message Queue.
ExactlyOnce Outra propriedade do tipo booleana e, quando definida como True (padrão), garantirá que uma vez que a mensagem for entregue ao serviço, ela não será duplicada. Caso a mensagem não seja entregue por algum motivo, a mesma será movida para a dead-letter queue. Devido a finalidade desta propriedade, obrigatoriamente a fila deverá ser transacional.
MaxRetryCycles A tentativa de processamento/entrega da mensagem (da fila para o serviço) consiste em ciclos, e cada ciclo contém um número de tentativas. Esta propriedade define um número inteiro que especifica a quantidade de ciclos de tentativas de entrega/processamento a serem realizadas. Como valor padrão, esta propriedade possui 2 ciclos.
QueueTransferProtocol Esta propriedade determinará qual será o meio de comunicação utilizado pelos gerenciadores do Message Queue de ambas as partes. Essa propriedade poderá ser configurada com uma das opções definidas pelo enumerador QueueTransferProtocol:

  • Native: Utiliza o protocolo nativo do Message Queue (padrão).
  • Srmp: Utiliza o protocolo Soap Reliable Messaging Protocol.
  • SrmpSecure: Utiliza o protocolo Soap Reliable Messaging Protocol Secure.

Os protocolos SRMP e SRMPS são utilizados quando desejamos expor a fila através do protocolo HTTP, mas isso está fora do escopo deste artigo. Caso a propriedade UseActiveDirectory estiver definida como True e algum protocolo diferente do Native for utilizado, uma exceção será disparada.

ReceiveErrorHandling Esta propriedade determinará o comportamento que o serviço WCF deverá ter quando todas as tentativas de entrega/processamento se esgotarem. Essa propriedade aceita uma das quatro opções fornecidas pelo enumerador ReceiveErrorHandling:

  • Fault: Quando um erro for encontrado ao processar a mensagem e a opção Fault estiver definida, o host será comprometido e a mensagem deverá ser removida da fila através do administrador ou outra aplicação antes de continuar a execução e, além disso, como esta opção compromete a vida do host, ele deverá ser reinicializado. Nenhuma notificação (Acknowledgement) do problema será enviada ao cliente.

  • Drop: Como o próprio nome indica, essa mensagem será efetivamente excluída da fila, continuando o processamento das outras mensagens. O Drop notifica (Acknowledgement) o cliente mas, do seu ponto de vista, a mensagem foi processada com sucesso. A mensagem será colocada na dead-letter queue do cliente caso a mensagem não tenha expirado (TimeToLive); caso contrário, ela não aparecerá em lugar algum.

  • Reject: Envia uma notificação (Acknowledgement) para o cliente informando que a mensagem não pode ser recebida pela aplicação (serviço). A mensagem é colocada na dead-letter queue do cliente. O cliente receberá uma notificação (Acknowledgement) negativa informando que a mensagem não pode ser processada. A mensagem será colocada na dead-letter queue do cliente.

  • Move: Move a mensagem para a Poison Message Queue, permitindo que a mensagem seja analisada posteriormente. Falaremos mais sobre esse tipo especial de fila ainda neste artigo. Nenhuma notificação (Acknowledgement) será enviada ao cliente, pois a idéia é enviá-la quando a mensagem poison for processada.

ReceiveRetryCount Cada ciclo (especificado através da propriedade MaxRetryCycles) possui uma quantidade de tentativas. Através desta propriedade, conseguimos determinar a quantidade de tentativas de cada ciclo. Como valor padrão, esta propriedade possui 5 tentativas.
RetryCycleDelay Esta propriedade define um intervalo de tempo (TimeSpan) entre os ciclos de tentativas. Como valor padrão, esta propriedade está definida como 10 minutos.
TimeToLive Essa propriedade define um TimeSpan indicando o período em que a mensagem irá expirar. Mensagens que estão dentro da fila e que não são acessadas por uma aplicação dentro do intervalo especificado serão expiradas, movendo-as para a dead-letter queue. Quando omitido, o valor padrão desta propriedade é 1 dia.
UseActiveDirectory O endereço do Message Queue consiste em dois formatos: path names ou direct format names. A primeira opção faz com que o Message Queue utilize o Active Directory para resolver o nome da fila (exemplo: NomeDoComputadorprivate$NomeDaFila), enquanto a segunda opção, tentará resolver o nome da fila utilizando o DNS ou o IP (exemplo: FormatName:Direct=TCP:192.168.1.3NomeDaFila).

Por padrão, o WCF converte a URI do serviço no formato direct format name e, através da propriedade UseActiveDirectory você poderá definir um valor booleano (que por padrão é False), fazendo com que ele utilize o formato path name. Algumas funcionalidades disponibilizadas pelo binding NetMsmqBinding, como é o caso da criptografia de mensagens utilizando a segurança a nível de transporte, somente funcionarão se utilizar o Active Directory.

UseMsmqTracing Valor booleano que indica se o processamento das mensagens será ou não logado. O valor padrão é False.
UseSourceJournal Journaling é um recurso do Message Queue que permite salvar uma cópia das mensagens entregues com sucesso ao destino. Por padrão essa funcionalidade está desabilitada, mas você pode definir a propriedade UseSourceJournal como True para colocar em funcionamento este recurso.

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

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

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.

MessageQueue.zip (98.24 kb)

Exceções dentro de Threads no ASP.NET

Quando uma exceção não tratada ocorre dentro na mesma thread que está executando a requisição, provavelmente teremos aquela famosa página amarela com os detalhes do erro, incluindo a mensagem, o tipo da exceção e a stack trace (vamos levar em conta que a seção customErrors está desabilitada), continuando a aplicação funcionando perfeitamente, ou seja, outras requisições não serão comprometidas.

Exceções não tratadas que ocorrem dentro da thread que executa a requisição não traz maiores problemas para a aplicação como um todo, ao contrário quando a exceção não tratada acontece dentro de uma thread a parte que, podem ser criadas através de uma instancia da classe Thread, através do método estático QueueUserWorkItem da classe ThreadPool ou, uma forma mais esotérica, quando alguma exceção acontece dentro de um destrutor de uma classe. Dependendo do que se precisa fazer, a utilização das páginas assíncronas garantem que um processo de IO seja efetuado em uma thread a parte mas, qualquer problema que eventualmente possa acontecer, a exceção sempre será disparada sob a thread que está executando a requisição.

Neste cenário, exceções não tratadas levam o AppDomain da respectiva aplicação ASP.NET ser reciclado e, com isso, todas as informações que estão armazenadas dentro do mesmo, como Session, Application e Cache serão perdidas, fazendo com que outros usuários sejam prejudicados. Aplicações que dependem destes dados, confiantes que uma vez definido nunca será nulo, podem começar a falhar e, um dos motivos podem ser justamente a questão de exceções não tratadas em threads.

Como essas exceções não tratadas não são catalogadas mesmo quando o Health Monitoring está habilitado, temos que recorrer ao conhecido evento UnhandledException da classe AppDomain. Podemos aqui utilizar um módulo para que todas as requisições façam o uso dele e, como exemplo, este artigo mostra detalhes da implementação deste importante módulo. Apesar de existir um contador de performance que contabiliza as reinicializações da aplicação, é sempre importante efetuar o log de possíveis exceções que podem estar sendo disparadas.

Palestra TechEd 2008

Como mencionei aqui, eu palestrei no TechEd Brasil 2008, falando sobre as possíveis formas de trabalhar assincronamente dentro do ASP.NET 2.0. Eu fiz uma aplicação de exemplo para demonstrar tais funcionalidades que, estou disponibilizando aqui para download. Com relação ao PPT, eu ainda não tenho permissão para publicar e, assim que puder, eu postarei aqui o link até o mesmo.

Gostaria de agradecer publicamente a todos que estiveram presentes e, um obrigado em especial ao Rodolfo Roim e Rogério Cordeiro, que me deram esta oportunidade.

WCF – Sincronização

Ao expor um serviço para que ele seja consumido, devemos nos atentar à possibilidade deste serviço ser acessado simultaneamente. Isso ocorre quando múltiplas requisições (threads) tentam acessar o mesmo recurso ao mesmo tempo. A possibilidade de acessos simultâneos poderá acontecer dependendo do tipo de gerenciamento de instância escolhido para o serviço. A finalidade deste artigo é mostrar as três opções fornecidas pelo WCF para tratar a concorrência e, além disso, exibir algumas das várias técnicas de sincronização fornecidas pelo .NET Framework e que poderão ser utilizadas em conjunto com o WCF.

É importante dizer que os modos de concorrência aqui abordados não garantem a segurança ao acesso à algum recurso compartilhado. Isso fica sob responsabilidade do desenvolvedor que está desenvolvendo o serviço e, para isso, como falado acima, devemos recorrer às classes já existentes dentro do .NET Framework que permitem a sincronização e proteção à tais recursos em um ambiente multi-threading. Há alguns casos onde a combinação entre o modo de gerenciamento de instância com o modo de concorrência já garantem isso e veremos mais abaixo como aplicá-los.

Quando uma requisição chega para um serviço, uma thread é retirada do ThreadPool para servir à requisição, mais precisamente, uma thread de I/O. Quando múltiplas requisições chegam ao serviço e ele, por sua vez, suporta que múltiplas requisições o acessem, então precisamos saber como tratá-las. Um ponto importante aqui é que a concorrência poderá ou não existir dependendo do modo de gerenciamento de instância do serviço. Para recapitular, temos três possibilidades: PerSession, PerCall e Single.

No modo PerSession uma instância será criada e mantida enquanto durar o proxy. Caso o cliente utilize o mesmo proxy para realizar várias chamadas ao mesmo tempo, então poderá haver concorrência entre as requisições de um mesmo cliente; já no modelo PerCall tradicional a concorrência não existirá, pois para cada chamada de uma determinada operação uma nova instância do serviço será criada para atendê-la. Infelizmente o modelo PerCall não está totalmente seguro em relação aos problemas de concorrência pois, o mesmo poderá fazer o uso de um recurso compartilhado entre várias chamadas (como caching, variáveis estáticas, etc.). Finalmente, o modelo Single é o modo de gerenciamento de instância mais propício aos problemas, já que toda e qualquer requisição será direcionada para a mesma instância e, caso os recursos compartilhados não estejam devidamente assegurados, podemos ter problemas de deadlocks ou contenção.

Para tratar a concorrência o WCF fornece uma propriedade chamada ConcurrencyMode, que está definida no atributo ServiceBehaviorAttribute, ou seja, é uma característica do serviço e não do contrato. Essa propriedade aceita uma das três opções definidas no enumerador ConcurrencyMode, as quais estão listadas abaixo:

  • Single: O serviço poderá aceitar apenas uma chamada por vez, não aceitando re-entradas (utilizadas em callbacks). Para as mensagens que chegarem enquanto uma requisição está sendo processada, ele aguardará até que o processo corrente seja finalizado. Quando a opção não é especificada, este valor é definido como padrão.

  • Reentrant: Neste modelo, o serviço poderá aceitar apenas uma chamada por vez, assim como o Single, mas permitirá que re-entradas sejam realizadas (utilizadas em callbacks), evitando assim o deadlock.

  • Multiple: Este modelo suporta multi-threading. Isso possibilitará que múltiplas threads entrem ao mesmo tempo no serviço e, caso existam recursos compartilhados, precisará utilizar técnicas para garantir o acesso seguro à eles, evitando possíveis deadlocks.

O exemplo abaixo ilustra como devemos proceder para configurar uma das opções de gerenciamento de concorrência na classe que representa o serviço:

using System;
using System.ServiceModel;

[ServiceBehavior(
    InstanceContextMode = InstanceContextMode.PerSession
    , ConcurrencyMode = ConcurrencyMode.Reentrant)]
public class ServicoDeClientes : ICliente
{
    //implementação
}

Observação importante: Antes de prosseguir é importante que você tenha conhecimento em algumas funcionalidades disponibilizadas pelo .NET Framework para sincronização e proteção de recursos em um ambiente multi-threading. Algumas destas funcionalidades serão abordadas neste artigo, mas o seu funcionamento não será comentado profundamente, já que isso está fora do escopo do artigo. Para um melhor entendimento destas funcionalidades, você poderá consultar o Capítulo 14 deste artigo.

Single

Quando a concorrência de um serviço é definida como Single, o mesmo será bloqueado enquanto uma requisição está sendo atendida, impossibilitando que outras requisições executem ao mesmo tempo. Se elas existirem, elas serão enfileiradas aguardando que o processo finalize para que elas sejam processadas, na respectiva ordem que chegaram. Esse modelo é o mais seguro, já que você não precisará lidar com a proteção de recursos compartilhados, pois o próprio WCF garante que apenas uma única thread será processada por vez.

Uma vez que a requisição corrente é encerrada, o WCF desbloqueia o serviço, permitindo que a próxima mensagem da fila (caso exista) seja processada. Quando esta mensagem começar a ser processada, ela bloqueia novamente o serviço, mantendo este bloqueio até que o método seja finalizado. Esse modo de concorrência diminui os problemas mas, da mesma forma, diminui o throughput.

Baseando-se no modo Single de concorrência, quando o modelo de gerenciamento de instância da classe estiver definido como PerSession e alguma requisição seja realizada para o serviço através do mesmo proxy, essa segunda chamada deverá aguardar até que a primeira seja completamente finalizada. No modelo PerCall não há concorrência, já que sempre haverá uma instância exclusiva para cada requisição e não sendo compartilhada com nenhuma outra chamada. Finalmente, se o modo de gerenciamento de instância estiver também definido como Single, ou seja, uma única instância servindo todas as requisições, apenas uma única chamada será processada por vez, bloqueando todas as outras, colocando-as em uma fila e não tendo problemas com concorrência.

Reentrant

Antes de falar do modo Reentrant, é necessário entender uma funcionalidade que é fornecida pelo WCF e que é o grande alvo deste modo de gerenciamento de concorrência: callbacks. Os callbacks possibilitam a comunicação bidirecional entre o cliente e o serviço e também são referidos como comunicação duplex. Neste tipo de comunicação será necessário definir um callback para que o contrato possa, em um determinado momento, disparar um método pré-definido do lado do cliente. A criação deste tipo de mensagem está fora do escopo do artigo e assume que você já tenha o respectivo conhecimento. Caso ainda não possua, você pode consultar este artigo.

Este modo de tratamento de concorrência segue a mesma idéia do modelo anterior (Single), ou seja, apenas é permitido uma requisição ser processada por vez; entretanto, esse modo possui uma customização: podemos deixar momentaneamente o serviço para a realização de alguma tarefa externa (como a chamada para outro serviço) e, via callback, voltarmos seguramente ao mesmo serviço, continuando a execução do mesmo, sem causar deadlock. É importante dizer que durante esta “janela”, possíveis requisições que estiverem na fila, aguardando o processamento, poderão entrar no serviço (em sua respectiva ordem). Quando a thread que deixou o serviço para executar uma outra tarefa for finalizada, ela será recolocada na fila para ser processada, ou melhor, dar continuidade no processamento que já existia antes do callback.

Quando o serviço está exposto como PerCall e ele exige uma re-entrada (isso ocorre quando há callbacks), então será necessário definí-lo como Reentrant para não causar deadlock com ela mesma; a re-entrada não sofrerá problemas com bloqueios em relação às outras requisições, pois no modo PerCall a instância somente servirá àquela requisição. Já quando o modo de gerenciamento de instância estiver definido como PerSession e o de concorrência como Reentrant, o callback será disparado e, quando o controle voltar para o serviço e já existir alguma outra requisição sendo executada, o retorno do callback será bloqueado até que a requisição corrente seja finalizada. Essa característica permite aumentar o throughput para clientes que fazem chamadas multi-threading, permitindo que enquanto o callback seja processado, outra chamada para o serviço possa ser executada. Finalmente, com o modelo de gerenciamento de instância definido como Single, o callback será disparado e, quando o controle voltar, ele será enfileirado caso exista alguma requisição em processamento.

Um cuidado especial que se deve ter ao utilizar o modo Reentrant é o gerenciamento de estado dos membros internos utilizados pelo serviço. Como vimos anteriormente, uma vez que o callback for executado, outras chamadas poderão ocorrer em paralelo, mas há um detalhe: quando o serviço for executar o callback, é importante que ele mantenha o serviço em um estado consistente para atender as outras requisições; da mesma forma, quando o callback retornar, é necessário atualizar as informações utilizadas pelo método, pois durante a chamada externa possivelmente outras chamadas foram realizadas, podendo modificar as informações que ele está (estava) utilizando.

A instância do proxy que iniciou o processo deverá ficar ativa até que o callback seja disparado pelo serviço e, caso isso não aconteça, uma exceção do tipo ProtocolException será disparada. Além disso, é importante saber que o callback é sempre executado em uma thread diferente da thread que iniciou a chamada para o método. Isso quer dizer que, quando estamos utilizando esta técnica em aplicações Windows Forms, devemos nos atentar quando precisarmos acessar controles do formulário a partir do método de callback. Isso se deve ao fato de que os controles são criados em uma thread diferente (a mesma que iniciou a chamada para o método através do proxy), o que nos obriga à acessá-los a partir dela. Há algumas facilidades que o Windows Forms fornece em conjunto com o .NET Framework, mas não serão abordadas aqui.

Multiple

Utilizar o modo de concorrência como Multiple em conjunto com o modelo de gerenciamento de instância PerSession ou Single permitirá que uma mesma instância atenda à diversas requisições (threads), aumentando assim o throughput. O problema que existe nestes cenários é a possibilidade do serviço utilizar um recurso compartilhado entre essas chamadas, o que obrigará a proteger manualmente tais recursos, utilizando as técnicas de sincronização existentes. No modelo PerCall a opção de concorrência Multiple é irrelevante, já que neste modo haverá sempre uma instância servindo cada requisição.

Dependendo dos recursos que estão sendo utilizados pelo serviço, devemos protegê-los do acesso concorrente. Se estiver acessando algum recurso que já possua o devido tratamento, então não há com que se preocupar. Por outro lado, se o serviço mantém recursos que são acessados por múltiplas threads, como é o caso de membros internos da classe, então será necessário protegê-los manualmente. Caso você não faça isso, é grande a probabilidade de ocorrer deadlocks, race conditions e o conflito de informações durante o processamento.

Comparativo

Instância/Concorrência Single Reentrant Multiple
PerSession
  • Limita o throughput em chamadas concorrentes
  • Chamadas concorrentes podem somente ser processadas entre diferentes proxies
  • Não há necessidade de trabalhar com mecanismos de bloqueio
  • Aumenta o throughput em chamadas concorrentes
  • Permite acesso multi-thread quando o callback está sendo executado
  • É necessário ter cuidados especiais com o estado dos membros internos do serviço
  • Permite acesso multi-thread através de um mesmo proxy
  • Há necessidade de trabalhar com mecanismos de bloqueio
PerCall
  • Obterá um deadlock 
  • Irrelevante, já que há uma instância do serviço exclusiva
  • Irrelevante, mas tendo cuidados especiais quando acessar um recurso externo e compartilhado (variáveis estáticas)
Single
  • Limita o throughput
  • Garantia de que o código não será acessado por várias requisições ao mesmo tempo
  • Não há necessidade de trabalhar com mecanismos de bloqueio
  • Enquanto um callback estiver sendo executado, a próxima requisição (caso exista) será executada
  • Aumenta o throughput quando há chamadas enfileiradas
  • É necessário ter cuidados especiais com o estado dos membros internos do serviço
  • Não há bloqueio à nível de serviço, podendo múltiplas requisições acessar a mesma instância
  • Há necessidade de trabalhar com mecanismos de bloqueio
  • O throughput está condicionado aos bloqueios realizados


Conclusão:
Este artigo mostrou as características de cada um dos modos de gerenciamento de concorrência que existe dentro do WCF. Apesar da configuração ser extremamente básica, isso muda drasticamente o comportamento do serviço, principalmente quando combinado com algum dos modos de gerenciamento de instância. Além disso, a escolha do modo de concorrência influenciará em como você deve escrever a implementação do serviço, se atentando ou não aos possíveis bloqueios que possam acontecer quando tentar acessar recurso compartilhado.

Multi-threading do lado do cliente

Quando optamos por utilizar um proxy que fornece apenas a versão síncrona de uma operação, devemos ter cuidado quando efetuamos a chamada para a mesma através de múltiplas threads do lado do cliente.

Ao invocar uma operação do proxy, raramente invocamos o método Open do mesmo, que tem a finalidade de abrí-lo explicitamente, obrigando ao runtime do WCF assegurar essa abertura, garantindo que a conexão seja estabelecida antes da mensagem ser enviada para o respectivo serviço. Isso quer dizer que nenhuma mensagem será enviada até que o proxy esteja completamente aberto.

Como estamos em um ambiente multi-threading do lado do cliente e postergando a abertura do proxy somente a partir da primeira chamada para uma operação, teremos aqui um comportamento que poderá causar alguma confusão. Como haverá múltiplas threads executando em paralelo utilizando o mesmo proxy, uma dessas threads ganhará a possibilidade de abrí-lo e, conseqüentemente, enviar a sua requisição para o serviço. Mesmo que o ambiente seja multi-threading, as outras threads que também tem como tarefa a execução de alguma operação ligada ao mesmo proxy, irão aguardar até que a primeira chamada seja completamente realizada.

Todas as requisições que forem realizadas enquanto a primeira delas (a responsável pela aberta do proxy) estiver em execução, serão armazenadas em uma fila (FIFO), executando sequencialmente cada uma dessas requisições solicitadas enquanto a primeira estava sendo processada. As classes (não documentadas) ServiceChannel e CallOnceManager (namespace System.ServiceModel.Channels) são as responsáveis por garantir essa sincronização. A classe CallOnceManager fornece um método chamado CallOnce que gerencia múltiplas requisições. Ao chegar uma requisição para este método, ele verificará se é ou não a primeira requisição solicitada ao proxy; caso seja, ele encaminhará para a execução e, caso não seja, ele irá armazenar essa requisição em uma fila.

Para resolvermos esse problema, podemos invocar as chamadas de forma assíncrona ou chamar explicitamente o método Open do proxy, não delegando a abertura do mesmo somente quando a primeira requisição for solicitada. O exemplo abaixo ilustra isso:

public partial class Form1 : Form
{
    private int _contador;
    private ContratoClient _proxy;

    public Form1()
    {
        InitializeComponent();

        this._proxy = new ContratoClient();
        this._proxy.Open();
    }

    private void button1_Click(object sender, EventArgs e)
    {
        new MethodInvoker(() => 
            this._proxy.EnviarMensagem(++this._contador)).BeginInvoke(null, null);
    }
}

Callbacks em Windows Forms

Como sabemos, ao desenvolver aplicações em Windows Forms, os controles são criados sob a thread que é iniciada junto ao processo. Com isso, os controles tem uma afinidade com esta thread e, sendo assim, toda e qualquer alteração em cima destes controles, será necessário que seja realizada através da própria thread que os criou.

Quando utilizamos o recurso de callbacks do WCF, é muito comum implementarmos a interface de callback no próprio formulário Windows, fazendo a atualização de controles que estão dentro do próprio formulário. Quando o callback for executado pelo runtime do WCF, ele será disparado em uma thread diferente da thread de criação dos controles e, seguramente, teremos uma exceção sendo disparada justamente devido a questão de afinidade que os controles possuem.

Em uma configuração padrão, ao fazer isso a exceção não ocorrerá. Isso se deve ao fato de que, se o método do proxy (isso envolve também o método Open) for invocado a partir de algum evento gerado pela UI (Form.Load, Button.Click, etc.), ele estabelecerá a afinidade com esta thread e, quando o callback acontecer, automaticamente ele será executado nesta mesma thread, ou seja, a mesma que efetuou a chamada e que também, é a mesma que criou os controles.

Isso é garantido porque o WCF faz o uso de uma classe chamada SynchronizationContext. Esta classe permite executarmos uma determinada tarefa em uma outra thread, diferente da qual estamos atualmente, representando uma espécie de canal entre as duas threads envolvidas. Quando o callback ocorrer, o WCF utilizará este mecanismo para executar o mesmo na própria thread de criação dos controles. Mas lembre-se de que isso somente acontece porque ao fazer a chamada, ele conseguiu estabelecer o contexto com a thread atual.

É importante dizer que voce pode controlar o uso do SynchronizationContext. Para isso, recorremos à propriedade booleana chamada UseSynchronizationContext, que é exposta através do atributo CallbackBehaviorAttribute, que pode ser aplicada à classe que implementa a interface de callback que, neste caso, é o próprio formulário Windows.

WCF, IIS e Threads

Sabemos que podemos utilizar o IIS como hosting para um serviço WCF. O Visual Studio 2008 já traz uma template chamada WCF Service. A finalidade desta template é possibilitar a criação de vários serviços, onde cada um será representado por um arquivo com a extensão *.svc. Depois de desenvolvido, podemos publicar este serviço em um servidor com IIS e com o .NET Framework devidamente instalado.

O WCF fornece dois tipos não documentados dentro do namespace System.ServiceModel.Activation, a saber: HttpHandler e HttpModule. A finalidade destes tipos é acoplá-los ao pipeline do ASP.NET e, quando uma requisição chegar para um serviço WCF (*.svc), o ASP.NET irá desviar a execução para o HttpHandler.

Um grande problema que ocorre neste procedimento é com relação ao uso de threads. Como a requisição é inicialmente passada pelo ASP.NET e, quando ele detecta que se trata de um serviço WCF, uma thread de I/O é criada para executar o serviço mas, a worker thread que é utilizada por tratar as requisições do ASP.NET, continuará bloqueada até que a requisição à operação esteja completa. Isso impedirá que a worker thread atenda à outras requisições enquanto espera que o processo seja finalizado.

A configuração padrão do WCF (mesmo depois do SP1 instalado) continua utilizando esta mesma técnica, ou seja, o processamento síncrono da operação. Mas, a partir do SP1 do .NET Framework 3.5, foram introduzidos mais dois tipos que tem o papel de executar, de forma assíncrona, as operações que chegam pelo ASP.NET ao WCF. Esses novos tipos são: ServiceHttpModule e ServiceHttpHandlerFactory.

Com estes dois novos tipos acoplados no pipeline do ASP.NET, permitirá que a execução seja feita de forma assíncrona, liberando a worker thread utilizada pelo ASP.NET para atender a outras requisições. Para habilitar o processamento assíncrono, podemos utilizar uma ferramenta chamada WcfAsyncWebUtil.exe, desenvolvida pelo Wenlong Dong.

É importante dizer que há uma relação entre o processamento assíncrono do runtime do ASP.NET/WCF com o contrato assíncrono (AsyncPattern = true). Se combinarmos o handler/módulo assíncrono com o contrato assíncrono, nenhuma thread será consumida durante a espera pelo processamento assíncrono da operação. Mesmo quando o contrato não é assíncrono, ao utilizar o handler/módulo assíncrono, teríamos uma diminuição de threads rodando concorrentemente e assim, tendo um ganho considerável.

TechEd Brasil 2008

Bem, como todo mundo já sabe, de 14 à 16 de outubro acontece em São Paulo o TechEd Brasil e, como no ano passado, este ano também estarei efetuando uma palestra na sala 7 no dia 15/10, das 11:00 às 12:15hs. O tema da palestra é:

WEB401 – Programação Assíncrona do ASP.NET 2.0
Nível: 400

Um dos principais pontos para a implementação de aplicações ASP.NET escaláveis, é o uso de pool de threads do ASP.NET da maneira mais eficiente possível. Isto significa evitar situações onde threads fiquem: aguardando o retorno de consultas à base de dados, chamadas à Web Services e que operações de I/O terminem. Nesta sessão descreveremos como ajustar os três modelos de programação assíncrona em ASP.NET. Se você está preocupado em construir sites Web com alta escalabilidade assista essa sessão.

Pré-Requisitos: Como pré-requisito para esta palestra é importante que o ouvinte tenha conhecimento nas classes de acesso à banco dados, uso de proxy para chamadas à Web Services e, se possível, familiaridade com o modelo de programação assíncrona (APM) do .NET Framework (Begin/End).

Além da palestra, estarei também participando do Ask The Experts, que é um bate-papo informal sobre determinadas tecnologias. Estarei na seção onde será abordado sobre ASP.NET, AJAX, Silverlight e Windows Live. Para aqueles que estiverem no evento e, se estiver disposto a assistir a palestra ou discutir sobre alguma tecnologia, fiquem a vontade para me procurar e, conseqüentemente, conversar à respeito dos assuntos relacionados a palestra e/ou algo sobre .NET.

Chamadas assíncronas à Serviço via modelo de eventos

O WCF fornece, não por padrão, a possibilidade de invocar as operações de um serviço de forma assíncrona. Quando efetuamos a referencia para um serviço via IDE do Visual Studio 2008 ou através do utilitário svcutil.exe (com a opção /async) automaticamente, além da versão síncrona do método, dois métodos chamados BeginXXX/EndXXX (onde XXX é o nome do método síncrono) são adicionados, permitindo que voce invoque assincronamente uma determinada operação.

Além do tradicional modelo de chamadas assíncronas (APM), temos a possibilidade da chamada assíncrona baseada em eventos. A idéia aqui é, antes de invocar a operação, voce poderá assinar à um evento que será disparado somente quando o processo assíncrono finalizar. Isso evitará de ter um trabalho manual para analisar se o processo finalizou ou não (poll, waiting, etc.). Internamente durante a geração do proxy, o código que é auto-gerado já inclui a implementação necessária para o modelo baseado em eventos.

Basicamente é criado mais uma versão do método, agora com o sufixo XXXAsync que, internamente, faz a chamada para os métodos BeginXXX/EndXXX que, como já sabemos, dispararam a operação de forma assíncrona. Além disso, um delegate do tipo EventHandler<T> também será criado para representar o callback que, quando disparado, invocará o evento do lado de quem está consumindo o serviço. Abaixo um exemplo de como efetuar a chamada assíncrona baseada em eventos:

using (ClienteClient p = new ClienteClient())
{
    p.CalcularComissaoCompleted += 
        new EventHandler<CalcularComissaoCompletedEventArgs>(p_CalcularComissaoCompleted);

    p.CalcularComissaoAsync(“2”);
    Console.ReadLine();
}

static void p_CalcularComissaoCompleted(object sender,
    CalcularComissaoCompletedEventArgs e)
{
    Console.WriteLine(“Fim.”);
}

Se notarmos a implementação interna do proxy, veremos que o método XXXAsync faz o uso do método InvokeAsync, da classe ClientBase<T>. Este método está disponível somente a partir do .NET Framework 3.5. Sendo assim, alguns detalhes durante a geração do proxy precisam ser analisados:

  • Via “Add Service Reference”: se voce estiver fazendo a referencia em um projeto que esteja utilizando o .NET Framework 3.5 e voce opta pela geração dos métodos que dão suporte ao processamento assíncrono, ele também criará os tipos necessários para suportar o modelo de eventos.
  • Via svcutil.exe: neste caso voce precisará especificar através do parametro /async e, além disso, especificar a versão do .NET Framework através do parametro /targetClientVersion, apontando para Version30 ou, se quiser utilizar o modelo baseado em eventos, utilizar a opção Version35.