Programação Assíncrona no ASP.NET MVC

by Israel Aece 25. January 2010 10:38

A Microsoft introduziu na versão 2.0 do ASP.NET WebForms uma funcionalidade chamada de páginas assíncronas, assunto qual já comentei bastante por aqui, e também fiz uma palestra no TechEd 2008 à respeito desse mesmo assunto. Para recapitular, e resumidamente falando, quando uma requisição chega para o IIS, o mesmo entrega para o ThreadPool da CLR, que utilizará uma thread para efetivamente executar aquela requisição.

Por padrão, as páginas são sempre síncronas, ou seja, enquanto aquela requisição não for finalizada, a thread não será liberada. O problema disso é que muitas vezes, as páginas executam processos pesados, como por exemplo, acesso à serviços, consultas em base de dados, etc. Essas tarefas são consideradas I/O-bound, ou seja, são tarefas que não dependem da máquina local, mas sim do processamento do computador remoto que hospeda o serviço/banco de dados, da latência da rede, etc. Neste tempo que a thread fica aguardando esse processo de I/O finalizar, ela poderia estar servindo outras requisições, também ASP.NET, mas que não fazem necessariamente acesso à recursos deste tipo, como por exemplo, páginas institucionais. Dependendo da demanda, é comum o usuário receber no navegador erros com as seguintes mensagens de erro: Server Unavaliable ou Server Too Busy (503).

As páginas assíncronas resolvem esse tipo de problema, ou seja, quando encontrar uma tarefa deste tipo, a executam em uma thread de I/O, devolvendo a thread para o ThreadPool, e dando a chance dela atender outras requisições. Quando o processo remoto finalizar, a thread de I/O é retornada com o resultado e, novamente, uma thread é apanhada do ThreadPool para finalizar a requisição, e que na maioria das vezes, irá renderizar o resultado.

Por ser algo que aumenta consideravelmente a performance, a Microsoft está introduzindo este mesmo recurso no ASP.NET MVC. A partir da versão 2.0 do mesmo, teremos a possibilidade de criar ações assíncronas. Aplicações MVC que estão sujeitas à uma grande quantidade de requisições, podem fazer as execuções das ações de forma assíncrona, trabalhando de forma bem semelhante ao WebForms, e dando também ao MVC o mesmo benefício.

O primeiro passo para a criação de ações assíncronas, é fazer com o controller herde da classe abstrata AsyncController e não apenas de Controller. Essa classe fornecerá toda a infraestrutura para que as ações sejam executadas assincronamente, mas é importante dizer que mesmo que o controller herde de AsyncController, ele ainda pode continuar executando ações síncronas. Ao contrário do modelo síncrono, ações assíncronas dependem de um par de métodos, sendo um que inicia a tarefa custosa, e o segundo que será disparado quando o processo for finalizado. Atualmente é necessário sufixar o nome da ação com os respectivos sufixos: "Async" e "Completed". Abaixo temos um exemplo de como fica a estrutura de um controller assíncrono:

public class UsuariosController : AsyncController
{
    public void ListagemAsync(int quantidade)
    {
        //...
    }

    public ActionResult ListagemCompleted(Usuario[] usuarios)
    {
        //...
    }
}

Repare que o primeiro método é definido como void, pois quem retorna o resultado para a View é o método que é disparado quando o processo for finalizado. Um outro detalhe importante é que o método sempre será referenciado ou acessado no navegador como "Listagem" e nunca como "ListagemAsync", pois os sufixos são somentes utilizados pelo runtime do ASP.NET.

Ao herdar da classe AsyncController, novas propriedades estão a nossa disposição, e uma delas é a AsyncManager, que retorna a instância de uma classe com o mesmo nome. Como o próprio nome diz, ela é a responsável por gerenciar as operações assíncronas. Essa classe fornece três propriedades: OutstandingOperations, Parameters e Timeout. A primeira delas, OutstandingOperations, retorna a instância de uma classe chamada OperationCounter, onde essa classe controla a quantidade de operações que foram inicializadas pela respectiva ação. Já a propriedade Parameters, serve como um dicionário de dados, que permite passar informações para o método de finalização (como o resultado, por exemplo) e, finalmente, a propriedade Timeout, onde podemos definir um número inteiro que representa a quantidade de milisegundos (padrão de 45000 (45 segundos)) que o ASP.NET irá aguardar até que a operação seja finalizado.

Como sabemos, o .NET Framework fornece duas formas para trabalho assíncrono: modelo APM (métodos Begin/End) ou o modelo de eventos. O MVC suporta as duas formas de trabalho, mas a implementação para cada uma delas é ligeiramente diferente. Independentemente de qual técnica você utilize para invocar, o método que é disparado quando o processo assíncrono é finalizado não mudará em nada. Abaixo temos a sua implementação, e podemos notar que ele nada sabe sobre questões assíncronas.

public ActionResult ListagemCompleted(Usuario[] usuarios)
{
    ViewData["Usuarios"] = usuarios;
    return this.View();
}

O próximo passo é codificar o método ListagemAsync, e vamos utilizar inicialmente o modelo de programação assíncrona do .NET (APM). Como exemplo, vamos consumir um serviço WCF que foi referenciado na aplicação. Lembre-se que ao referenciar um serviço WCF, por padrão, ele não traz as versões assíncronas das operações; para poder habilitá-las, consulte este artigo.

Vamos então instanciar o proxy para estabelecer o canal de comunicação entre a aplicação e o serviço WCF. Podemos notar que temos que obrigatoriamente invocar os métodos Increment e Decrement, expostos pela propriedade OutstandingOperations, para especificar a quantidade de operações assíncronas que estão em andamento. Depois disso, devemos inicializar a operação assíncrona, através do método BeginRecuperarUsuarios. De acordo com o modelo APM, além dos parâmetros exigidos pelo método em si, temos que informar um callback, que nada mais é do que o método que será executado quando o processo assíncrono for finalizado.

Note no código abaixo que dentro do método de callback, estamos recuperando o resultado (EndRecuperarUsuarios) e armazenando dentro da propriedade Parameters a coleção de usuários. O valor colocado dentro deste dicionário será passado para o método ListagemCompleted, através do parâmetro "usuarios". Em seguida estamos também decrementando o contador de operações assíncronas. Note que tudo o que foi descrito neste parágrafo, está sendo executado dentro do método chamado Sync, também fornecido pela propriedade AsyncManager. Isso é necessário para garantir que este código e, um pouco mais tarde, a execução do método ListagemCompleted, sejam disparados em uma thread que o ASP.NET terá o controle. Se você não se atentar à isso e tentar executar esse código, ainda estará em uma thread de I/O, fazendo com que o contexto do HTTP (HttpContext.Current) esteja nulo e, consequentemente, não conseguirá acessar grande parte dos recursos que precisa para exibir o resultado.

public void ListagemAsync(int quantidade)
{
    ServicoDeUsuarios proxy = new ServicoDeUsuarios();
    this.AsyncManager.OutstandingOperations.Increment();

    proxy.BeginRecuperarUsuarios(quantidade, ar =>
    {
        AsyncManager.Sync(() =>
        {
            this.AsyncManager.Parameters["usuarios"] = proxy.EndRecuperarUsuarios(ar);
            this.AsyncManager.OutstandingOperations.Decrement();
        });
    }, null);
}

Depois de visualizar a implementação baseada no modelo APM, temos agora o modelo de eventos. Neste caso não precisamos envolver o método Sync, pois o evento que determina que o processo foi finalizado já acontece dentro da thread do próprio ASP.NET. Tudo o que precisamos fazer é se vincular à este evento, e dentro dele armazenar o resultado na propriedade Parameters e decrementar o contador, tudo de forma bem parecida ao que vimos acima. Apenas para iniciar o processo assíncrono, você deverá invocar a versão assíncrona do método que é gerado durante a criação do proxy do WCF, que terá sempre o nome da operação sufixada com a palavra "Async".

public void ListagemAsync(int quantidade)
{
    ServicoDeUsuarios proxy = new ServicoDeUsuarios();
    this.AsyncManager.OutstandingOperations.Increment();

    proxy.RecuperarUsuariosCompleted += (sender, e) =>
    {
        this.AsyncManager.Parameters["usuarios"] = e.Result;
        this.AsyncManager.OutstandingOperations.Decrement();
    };

    proxy.RecuperarUsuariosAsync(quantidade);
}

Observação: Qual dos dois modelos utilizar? Isso vai depender da API que está sendo chamada dentro do controller/ação assíncrono. Se ela suportar os dois modelos, então você pode escolher um deles. Mas há situações que não temos esse luxo, como por exemplo, quando queremos invocar uma consulta no SQL Server usando o ADO.NET tradicional, ou até mesmo ler o conteúdo de um arquivo no disco. Essas classes apenas fornece o modelo APM, com um par de métodos Begin/End.

A Microsoft ainda disponibilizou dois atributos: AsyncTimeoutAttribute e NoAsyncTimeoutAttribute. O primeiro deles disponibiliza uma propriedade chamada Duration, que como falamos acima, recebe a quantidade de milisegundos que determina o timeout. Já o segundo atributo deve ser aplicado quando você quer deixar isso indefinido. Independentemente de qual irá utilizar, eles devem ser aplicados sempre ao método que está sufixado com a palavra "Async", assim como vemos abaixo:

[AsyncTimeout(Duration = 30000)]
public void ListagemAsync(int quantidade)
{
    //...
}

Conclusão: É importante dizer que esse modelo de programação, apesar de tornar o código um pouco mais ilegível e poluído, traz um grande benefício em termos de performance e escalabilidade. Não pense que a página aparecerá no navegador do usuário enquanto a ação é processada, e quando ela for finalizada, aparecerá os dados na tela. Em termos visuais, você ainda terá o mesmo resultado, ou seja, enquanto a ação não for finalizada o navegador ficará bloqueado até que a requisição como um todo seja concluída, mas esta funcionalidade irá "desafogar" o ASP.NET.

Tags: ,

ASP.NET | Async

Threading em WPF

by Israel Aece 24. November 2009 21:27

Quando desenvolvemos aplicações Windows, é muito comum em algum ponto da mesma, que algumas tarefas mais complicadas e custosas sejam realizadas, que podem levar um tempo maior até que seja concluída. Independentemente do que ela faça, seja um cálculo, uma consulta em uma base de dados ou uma chamada para um serviço, se você executar esse código de forma síncrona, o usuário deverá esperar até que essa tarefa seja finalizada, para a partir daí, conseguir acessar outras áreas do sistema.

Ao rodar uma aplicação Windows, um processo é criado dentro do sistema operacional. Processo não executa nenhum código; são as threads que fazem isso. Quando o processo é criado, uma thread é criada juntamente com ele, e esta é muitas vezes chamada de main-thread (thread principal). Essa thread nasce e morre com o término do processo, ou seja, enquanto ela estiver executando alguma tarefa, o processo continuará ativo.

As aplicações Windows que conhecemos, como Windows Forms, Console, Windows Services e WPF trabalham nesta mesma linha. Aplicações que possuem gráficos, como é o caso do Windows Forms e do WPF, tem um agravante: a afinidade que os controles tem com a thread principal. Quando a aplicação é iniciada, a thread principal é quem cria os controles (Form, TextBox, Label, TextBlock, etc.), e quando dizemos que há uma afinidade, isso quer dizer que podemos somente manipular esses controles, através da mesma thread que os criaram, e qualquer tentativa de fazer isso através de uma segunda thread, uma exceção do tipo InvalidOperationException será disparada.

Quando essas tarefas são finalizadas, é normal queremos exibir o resultado para o usuário, que na maioria das vezes, implica em alterar a propriedade Text de algum controle, exibir uma MessageBox, etc. Em Windows Forms, todos os controles herdam direta ou indiretamente da classe Control, que fornece um método chamado Invoke, e que dado um delegate, executa o método relacionado na mesma thread do criador. Mais tarde, com o .NET Framework 2.0, surgiu o SynchronizationContext, que facilitou bastante a atualização dos controles a partir de uma thread secundária.

Agora temos o WPF, que traz novas funcionalidades e uma forma um pouco diferente para lidar com esse tipo de problema. Vamos a partir deste artigo, explorar um pouco mais sobre o modelo de threading do WPF. A Microsoft introduziu uma série de novos tipos, espalhados por vários namespaces e que serão utilizados para conseguir atingir o nosso principal objetivo. Para iniciar, o primeiro tipo que temos que conhecer é a classe Dispatcher. Essa classe serve como um gerenciador de tarefas para serem executadas, e está sempre associada com uma determinada thread de UI. Ela mantém uma fila de tarefas que são executadas utilizando a thread qual está relacionada.

A fila que é mantida pela classe Dispatcher é priorizada, que permite especificar uma prioridade antes de enfileirar a tarefa (mais detalhes abaixo). Para alistar uma tarefa nesta fila, você poderá utilizar o método Invoke ou BeginInvoke. A diferença entre eles é clara: o primeiro executa a tarefa de forma síncrona, enquanto a segunda alista a tarefa para ser executada de forma assíncrona. E para sedimentar, ambas sempre executarão na thread ao qual o Dispatcher está vinculado.

Grande parte das classes que compõem o framework do WPF, incluindo os controles, herda direta ou indiretamente da classe abstrata DispatcherObject, que possui uma estrutura simples, ou seja, fornece uma propriedade chamada Dispatcher que retorna a instância de uma classe Dispatcher, e como já era de esperar, fornece a instância do Dispatcher que está vinculado com aquele classe/controle.

A classe DispatcherObject ainda fornece dois métodos importantes: CheckAccess e VerifyAccess. A diferença entre eles é que o primeiro retorna um valor boleano, indicando se a thread que está chamando tem direito de acesso ao Dispatcher correspondente. Já o segundo método, VerifyAccess, dispara uma exceção do tipo InvalidOperationException, caso a thread que está chamando não tiver direito de acesso ao Dispatcher. Como pudemos perceber, esses métodos vão nos auxiliar para determinar se há ou não a necessidade de atualizar o controle através da thread atual, sem que seja necessário utilizar o Dispatcher para chegar até o controle.

Para exemplificar, imagine que temos uma thread que executará algum cálculo complexo, e depois de calculado, deverá exibir o resultado em um TextBox. Como comentado acima, dentro desta thread não podemos alterar qualquer propriedade do TextBox, e para solucionar isso no WPF, vamos recorrer a propriedade Dispatcher do TextBox ("txt"), que foi herdada de DispatcherObject. Ao passar um delegate para o método Invoke, ele será executado (de forma síncrona) na mesma thread que criou o controle.

new Thread(() =>
{
    int result = 2 ^ 4 * 2 + 3 / 3;
    Thread.Sleep(3000); //Simula Processo Complexo

    txt.Dispatcher.Invoke(new Action<int>(r => txt.Text = r.ToString()), result);
}).Start();

Se desejar, podemos trocar o método Invoke por BeginInvoke, e a atualização do controle será feita em background. A vantagem desta técnica é que você pode executar a atualização do controle enquanto faz outras tarefas. É importante que você mantenha tarefas "leves" dentro do Dispatcher, pois tudo o que ele deveria fazer ali é atualizar a UI; colocar tarefas mais complexas, voltará a ter concorrência com os eventos dos controles e, consequentemente, o usuário voltará a ter os travamentos das telas do sistema, que acontecia quando trabalhávamos de forma síncrona.

Quando você opta por utilizar o método BeginInvoke, ele retorna uma instância da classe DispatcherOperation. Basicamente, este objeto representa uma espécie de "ponteiro" para a operação que está sendo executada. Essa classe fornece uma série de membros interessantes, e entre eles temos:

  • Dispatcher: O Dispatcher relacionado.
  • Priority: Uma das opções definidas no enumerador DispatcherPriority, que define a prioridade da operação.
  • Result: Retorna um System.Object com o resultado da tarefa (isso quando ela retornar algum resultado).
  • Status: Uma das opções definidas no enumerador DispatcherOperationStatus, que define o status atual da operação (Pending, Aborted, Completed ou Execution).
  • Abort: Método que aborta a operação que está sendo executada.
  • Wait: Quando invocado, fará um "join" na thread atual, aguardando até o término da operação. Opcionalmente você pode especificar um timeout.
  • Aborted: Evento que é disparado quando a operação é abortada.
  • Completed: Evento que é disparado quando a operação foi finalizada.

Com a instância do DispatcherOperation em mãos, podemos utilizar duas formas para chegar até o resultado, que é via eventos ou através de polling. Utilizando o modelo de eventos, podemos nos vincular ao evento Completed, e quando a operação for finalizada, esse evento será automaticamente disparado. Já o polling consiste em testar, de tempo em tempo, se a operação finalizou ou não. Abaixo temos os dois exemplos de utilização:

new Thread(() =>
{
    int result = 2 ^ 4 * 2 + 3 / 3;
    Thread.Sleep(3000); //Simula Processo Complexo

    DispatcherOperation op =
        txt.Dispatcher.BeginInvoke(new Action<int>(r => txt.Text = r.ToString()), result);

    op.Completed += (o, args) => MessageBox.Show("Finalizou Tudo");
}).Start();


new Thread(() =>
{
    int result = 2 ^ 4 * 2 + 3 / 3;
    Thread.Sleep(3000); //Simula Processo Complexo

    DispatcherOperation op =
        txt.Dispatcher.BeginInvoke(new Action<int>(r => txt.Text = r.ToString()), result);

    //Faz Algo...

    while (op.Status != DispatcherOperationStatus.Completed)
    {
        if (op.Wait(TimeSpan.FromSeconds(5)) == DispatcherOperationStatus.Completed)
        {
            //Finalizou a Atualização da UI
            break;
        }
    }
}).Start();

Tanto o método Invoke quanto o BeginInvoke possui versões (overloads) destes métodos que permitem especificar uma prioridade, e para isso, utilizamos uma das doze opções definidas pelo enumerador DispatcherPriority. Há algumas opções interessantes, como por exemplo Inactive, que permite você alistar a operação, mas que não será processada. De qualquer forma, na maioria das vezes a opção Normal (que também é o padrão), já será o suficiente. Abaixo um exemplo de como podemos proceder para especificar a prioridade de uma operação:

txt.Dispatcher.BeginInvoke(new Action<int>(r => txt.Text = r.ToString()), DispatcherPriority.Normal, result);

Uma vez que você tem operações alistadas no Dispatcher, o runtime irá determinar quando executá-las, dependendo da prioridade definida. A classe Dispatcher também fornece métodos para abortar todas as operações pendentes de processamento. Para isso, recorremos aos métodos InvokeShutdown ou BeginInvokeShutdown (a diferença entre eles já é sabida). A classe Dispatcher ainda fornece um evento chamado ShutdownFinished, que é disparado quando shutdown do Dispatcher for completamente finalizado, e com isso, você poderá tomar alguma decisão.

Conclusão: Como vimos, ambas tecnologias (Windows Forms e WPF) possuem os mesmos comportamentos. Como trabalhar com ambientes multi-threading não é uma tarefa fácil, a Microsoft introduziu no WPF uma forma diferente de se trabalhar para a atualizar de UI através de uma segunda thread. Além de um modelo suavemente diferente, há algumas melhorias internas que garantem que isso acabe sendo executado de uma forma melhor. E para finalizar, essa técnica visa sempre tornar a aplicação mais amigável para o usuário, sem que eles tenha experiências ruins.

Tags: ,

Async | WPF

Mensagens Assíncronas com ChannelFactory

by Israel Aece 18. November 2009 10:41

Uma das formas que temos para consumir serviços WCF em um cliente qualquer, é utilizando a classe ChannelFactory<TChannel>, assim como eu já mostrei um exemplo neste post. Geralmente utilizamos esta técnica quando optamos por compartilhar os tipos (incluindo a interface que representa o contrato) entre o serviço e o cliente, evitando a publicação do documento WSDL, a reconstrução destes tipos do lado do cliente e, principalmente, a necessidade de efetuar a referência do serviço através da IDE do Visual Studio .NET.

Atenção: Antes de prosseguir a leitura deste post, aconselho que leia os seguintes artigos:

Quando fazemos a referência ao serviço através do Visual Studio, há uma opção chamada "Generate asynchronous operations", que para cada operação encontrada no serviço, um par de métodos BeginNomeDaOperacao e EndNomeDaOperacao serão criados, e que irão trabalhar em conjunto, para que assim, o cliente consiga invocar a respectiva operação de forma assíncrona. O problema disso é que muitas vezes o contrato não oferece suporte à chamadas assíncronas, o que nos obrigará a criar toda a infraestrutura do lado do cliente para suportar isso. Imagine que temos o seguinte contrato:

[ServiceContract]
public interface IContrato
{
    [OperationContract]
    string FazAlgo(string value);
}

Como não temos a versão assíncrona do método FazAlgo, temos que recorrer à delegates para conseguir efetuar a chamada. Sendo assim, o código do lado do cliente, poderia ficar da seguinte forma:

private ChannelFactory<IContrato> _factory;
private IContrato _proxy;

public Form1()
{
    this._factory =
        new ChannelFactory<IContrato>(
            new NetTcpBinding(),
            "net.tcp://localhost:8722/srv");

    this._proxy = this._factory.CreateChannel();
}

private void button1_Click(object sender, EventArgs e)
{
    string parametro = "algum valor";

    Func<string, string> executor = p => this._proxy.FazAlgo(p);

    executor.BeginInvoke(
        parametro,
        result =>
        {
            this.textBox1.Invoke(
                new Action<string>(valor => this.textBox1.Text = valor),
                ((Func<string, string>)result.AsyncState).EndInvoke(result));

        },
        executor);
}

A diferença que vemos no código acima, é que invocamos o método FazAlgo através de um delegate. Ao invés de criarmos delegates a todo momento para uma necessidade específica, podemos recorrer aos delegates expostos pelo .NET Framework Action<> e Func<>. No caso acima, estamos fazendo uso do Func<string, string>, que coincide com a assinatura do método FazAlgo, ou seja, recebe e devolve uma string. No exemplo acima, estamos fazendo o uso de lambda ao invés de explicitamente criar a instância do delegate.

Todos os delegates dão suporte à chamada assíncrono para método que ele mantém a referência, e sendo assim, via BeginInvoke disparamos a execução da operação do serviço, utilizando uma segunda thread, e mantendo a aplicação disponível para outros trabalhos. Neste caso, o método BeginInvoke recebe três parâmetros: o primeiro é o parâmetro de entrada que o serviço recebe; o segundo é um delegate de callback, que será invocado quando o resultado voltar; e finalmente, o terceiro parâmetro é um System.Object que será passado para o callback, e que estará acessível através da propriedade AsyncState, e que no caso, passamos a instância do delegate criado para invocar o método.

Para recuperar o resultado, dentro do callback invocamos o método EndInvoke, que retornará o resultado do serviço, e como na maioria dos casos, precisamos exibir isso na tela. Como o callback sempre é disparado na thread que está executando o método assíncrono, você não pode tocar nos controles, já que eles tem afinidade com a thread de criação deles. Aqui entra em cena o método Invoke, exposto pelo do controle que deseja atualizar, e através dele, determinamos um método para ser executado na mesma thread que o criou, e aqui também utilizando lambda.

Se você tiver acesso ao contrato e poder alterá-lo, então você pode dar suporte ao processamento assíncrono ao mesmo, e grande parte do trabalho que vimos acima, do lado do cliente, será descartado. Para suportar o processamento assíncrono no contrato, temos que criar as versões assíncronas do método, e com isso, o nosso contrato de exemplo ficará da seguinte forma:

[ServiceContract]
public interface IContrato
{
    [OperationContract]
    string FazAlgo(string value);

    [OperationContract(AsyncPattern = true)]
    IAsyncResult BeginFazAlgo(string value, AsyncCallback callback, object state);

    string EndFazAlgo(IAsyncResult result);
}

Ao contrário do que vimos anteriormente, ao utilizar essa técnica, você terá o processamento assíncrono tanto do lado do cliente, quanto do lado do serviço. Eu já discuti bastante sobre isso nestes outros artigos. Sendo assim, toda a complexidade da chamada assíncrona será movida para o serviço, que deveremos criar a versão assíncrona do método FazAlgo. O código do lado do cliente ficará relativamente mais simples:

private void button2_Click(object sender, EventArgs e)
{
    string parametro = "algum valor";

    this._proxy.BeginFazAlgo(
        parametro,
        result =>
        {
            this.textBox1.Invoke(
                new Action<string>(valor => this.textBox1.Text = valor),
                this._proxy.EndFazAlgo(result));
        }, null);
}

Para ajudar no entendimento, você pode baixar o código de exemplo clicando aqui.

Tags: , , ,

Async | WCF

.NET Reactive Framework

by Israel Aece 14. October 2009 08:30

A Microsoft trabalha atualmente em um projeto chamado de .NET Reactive Framework (Rx). Em uma linguagem como o C#, este framework ajudará, ou melhor, mudará a forma com a qual escrevemos código para lidar com eventos e também com a programação assíncrona, que tem as características de reagirem quando algo acontece, por exemplo, quando o evento Click ocorre dentro da classe Button, um método é disparado para que a nossa aplicação reaja a ele; na programação assíncrona, utilizamos delegates para especificarmos callbacks, ou seja, quando o processo assíncrono finalizar, dispare o método "XPTO".

Quando dizemos que teremos uma outra forma de trabalhar depois deste framework, é porque ele permitirá o que chamamos de LINQ To Events. Isso quer dizer que poderemos, de forma declarativa, definir uma sequência/combinação de eventos que estamos interessados, e quando ele(s) acontecer(em), nós seremos notificados. Antes de visualizar códigos que mostram isso, vamos recapitular como o Linq To Objects trabalha: basicamente tudo é baseado nas interfaces IEnumerable<T> e IEnumerator<T>, que utilizam o conceito de "pull" (puxar) para extrair os dados, iterando por alguma fonte de dados de forma síncrona.

Erik Meijer, que é o inventor do Rx Framework, descobriu que toda sequência que trabalha no formato "pull", também pode trabalhar no formato "push" (empurrar), ou seja, são matematicamente denominadas como "Dual". Para suportar a programação reativa, não podemos continuar utilizando as mesmas interfaces, justamente porque elas bloqueiam até que um próximo resultado esteja disponível. Sendo assim, a Microsoft introduziu duas novas interfaces: IObservable<T> e IObserver<T>. Através destas interfaces, você assinará uma coleção/tarefa qualquer, e quando uma nova informação estiver disponível, você será notificado, reagindo à este item.

Já vi comentários que isso será parte integrante ao .NET Framework 4.0, mas os tipos necessários para fazer isso tudo funcionar, estão contidos no assembly System.Reactive.dll, que vem com o Toolkit do Silverlight 3.0. A primeira interface, IObservable<T>, possui apenas um método chamado Subscribe. Este método recebe como parâmetro a instância de uma classe que implementa a interface IObserver<T>, que possui três métodos autoexplicativos: OnCompleted, OnError e OnNext. Em um exemplo simples, vamos percorrer uma coleção de números inteiros através desta nova forma. O primeiro passo é criar o observer:

public class VerificadorDeItens : IObserver<int>
{
    public void OnCompleted()
    {
        Console.WriteLine("Acabou");
    }

    public void OnError(Exception exception)
    {
        Console.WriteLine("Erro");
    }

    public void OnNext(int value)
    {
        Console.WriteLine(value);
    }
}

Depois dele devidamente criado, precisamos criar o "observable". Ao invés de fazermos isso explicitamente, a Microsoft também disponibilizou uma classe chamada Observable, que está dentro namespace System.Linq. Essa classe fornece uma série de métodos de extensão, para transformar grande parte dos operadores LINQ em "observables". Um desses métodos, ToObservable, é responsável por transformar um IEnumerable<T> em um IObservable<T>. Sendo assim, para percorrer um array de números inteiros no formato "push", fazemos:

int[] ids = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
ids.ToObservable().Subscribe(new VerificadorDeItens());

O que vimos acima, é somente um exemplo da diferença entre os modelos "pull" e "push", mas o mais interessante é a capacidade de manipular, ou melhor, de reagir à eventos. Ao invés de criar os tratadores (event handlers) tradicionais, você poderá "assinar" esse evento, e quando ele acontecer, você será notificado. Veja o exemplo abaixo:

Observable
    .FromEvent<EventArgs>(this.button1, "Click")
    .Subscribe(() => MessageBox.Show("Ocorreu o evento Click"));

Um ponto importante aqui é que o método Subscribe retorna a instância de uma classe que implementa a interface IDisposable, que poderá ser utilizada mais tarde, quando não quisermos mais ser notificados. Abaixo temos o mesmo exemplo, só que se descartando as notificações quando não precisamos mais delas, e para isso, tudo o que precisamos fazer é invocar o método Dispose:

IDisposable dispose =
    Observable
        .FromEvent<EventArgs>(this.button1, "Click")
        .Subscribe(() => MessageBox.Show("Ocorreu o evento Click"));

//faz algo aqui, e quando não estiver mais
//interessado nas notificações, invoque o
//método Dispose.
dispose.Dispose();

Tudo isso visa, principalmente, a capacidade que teremos que efetuar queries LINQ em eventos, que como falei acima, permite vincular à eventos, e na medida com que eles forem sendo disparados, você será notificado de forma assíncrona. Para isso, vamos imaginar que temos duas classes diferentes, onde cada uma delas possui um evento específico, e um método que executa alguma tarefa, e quando necessário, invoca o respectivo evento. Abaixo temos as duas classes que utilizaremos como exemplo. Repare que não há nada de especial:

public class Classe1
{
    public event EventHandler MeuEvento;

    public void Executar()
    {
        if (this.MeuEvento != null)
            this.MeuEvento(this, EventArgs.Empty);
    }
}

public class Classe2
{
    public event EventHandler MeuOutroEvento;

    public void Executar()
    {
        if (this.MeuOutroEvento != null)
            this.MeuOutroEvento(this, EventArgs.Empty);
    }
}

Com essas classes criadas, vamos então utilizar o LINQ para interagir com os eventos:

Classe1 c1 = new Classe1();
Classe2 c2 = new Classe2();

IObservable<Event<EventArgs>> obsC1 =
    Observable.FromEvent<EventArgs>(c1, "MeuEvento");

IObservable<Event<EventArgs>> obsC2 =
    Observable.FromEvent<EventArgs>(c2, "MeuOutroEvento");

var q =
    from in obsC1
    from y in obsC2
    select new Unit();

q.Subscribe(() => MessageBox.Show("Os eventos aconteceram!"));

c1.Executar();
c2.Executar();

Depois de instanciado a classe, utilizamos o método FromEvent, que dado o objeto e o nome do evento, cria um "observable" para o evento especificado. Com eles criados, o próximo passo é escrever a query LINQ, onde aninharemos os dois eventos na mesma query. Assim como toda query LINQ To Objects retorna um IEnumerable<T>, LINQ To Events retorna um IObservable<T>. Com isso, podemos utilizar o método Subscribe, para sermos notificados quando ambos eventos acontecerem. Assim que código executa o método "Executar" de "c1" e "c2", a mensagem será exibida. Se comentarmos qualquer um dos métodos "Executar", a mensagem não será disparada, pois de acordo com a query, nós estamos interessados em sermos notificados somente quando ambos eventos acontecerem.

Além de tudo isso, o .NET Reactive Framework ainda facilita muito a programação assíncrona. Sempre quando precisamos executar alguma tarefa em uma thread isolada, precisamos criar delegates, iniciar a tarefa através do método BeginInvoke, apontar (também via delegate) para o método de callback e, finalmente, recuperar o resultado utilizando o método EndInvoke. Isso é extremamente trabalhoso, e a complexidade aumenta ainda mais quando não se conhece exatamente como isso funciona. Suponhamos que temos a seguinte tarefa ser realizada:

public int Tarefa(int tempo)
{
    //Tarefa Custosa

    tempo = tempo * 1000;
    Thread.Sleep(tempo);
    return tempo;
}

Ao invés de criar toda aquela parafernalha para invocar e receber o resultado assincronamente, utilizando este novo framework, tudo o que precisamos fazer é:

new Func<int, int>(Tarefa)
    .ToAsync()
    .Invoke(3)
    .Subscribe((tempo) => { MessageBox.Show("Tempo Esperado: " + tempo.ToString()); });

No código acima, instanciamos o delegate Func<TResult, T> (que é bem conhecido), especificando que ele receberá e retornará um número inteiro. A partir daí, entra em cena o método de extensão chamado ToAsync (que foi aplicado aos delegates), que retorna uma versão assíncrona do mesmo delegate, e como já vimos acima, utilizamos o método Subscribe para criar o "observable", que nos trará o resultado do processo, isso quando estamos interessados nele.

Esses são apenas alguns - pequenos - exemplos de alguns cenários que podemos utilizar esta novo framework, mas já podemos notar o quanto ganhamos em certas situações, como é o caso da programação assíncrona. Há muito ainda o que se explorar e, provavelmente, isso tende a evoluir cada vez mais, tornando assim, a programação, ou melhor, o C#, cada vez mais funcional.

Tags:

.NET Framework | Async

WCF Vídeo - Mensagens Assíncronas

by Israel Aece 22. August 2009 22:56
Mensagens Assíncronas Mensagens Assíncronas

Para tornar a comunicação entre o cliente e serviço mais interativa, uma boa opção é invocar as operações de forma assíncrona. Este vídeo exibirá como podemos proceder para consumir um serviço utilizando esta técnica. Para maiores detalhes, consulte este artigo.

Formato: WMV - Duração: 00:21:34 - Tamanho: 26MB

Tags: ,

Async | WCF

Métodos assíncronos do ASP.NET Web Services

by Israel Aece 22. January 2009 15:12

Ao fazer a referencia para um ASP.NET Web Services (ASMX), automaticamente o proxy é gerado. Antes do WCF, ao referenciar serviços ASMX a versão assíncrona (BeginXXX/EndXXX) dos métodos expostos por ele também eram criados.

Utilizando o Visual Studio .NET 2008, esse comportamento mudou um pouco. Agora temos uma opção chamada "Add Service Reference..." que, dado um endereço (seja ele para um serviço ASP.NET Web Services ou WCF), irá gerar o proxy. A questão é que este proxy baseia-se na infraestrutura do WCF (ClientBase<TChannel>), e a geração dos respectivos métodos assíncronos somente acontecerá se a opção "Generate asynchronous operations" do botão "Advanced" estiver selecionada (maiores detalhes neste artigo). Do contrário, a única forma assíncrona de trabalhar é utilizando o modelo de eventos.

Se quiser continuar gerando o proxy da forma antiga, ou seja, aquele que herda da classe SoapHttpClientProtocol, será necessário recorrer ao utilitário wsdl.exe, como é mostrado abaixo:

C:\>wsdl http://localhost:54509/WebService1/Service.asmx /out:C:\Temp\Proxy.cs

Tags:

ASP.NET | Async

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

by Israel Aece 11. January 2009 08:20

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)

Tags: ,

ASP.NET | Async | Data

WCF - MessageQueue

by Israel Aece 15. December 2008 15:53

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: NomeDoComputador\private$\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.3\NomeDaFila).

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)

Tags:

Async | CSD | WCF

Powered by BlogEngine.NET 1.5.0.0
Theme by Mads Kristensen

Sobre

Meu nome é Israel Aece e sou especialista em tecnologias de desenvolvimento Microsoft, atuando como desenvolvedor de aplicações para o mercado financeiro utilizando a plataforma .NET. [ Mais ]

Twitter

Host