Executando serviço em Background na infraestrutura do Asp.Net    

Olá pessoal!

Hoje eu vou abordar um tema não muito ortodoxo: As possíveis formas de “hospedar” um serviço na infraestrutura do Asp.Net.

Entenda como serviço nesse contexto uma tarefa que roda de tempos em tempos para realizar uma tarefa qualquer, que não dependa de uma ação do usuário, como o disparo de e-mails de newsletter, ou o que for. Não entenda serviço aqui como um WebService.

Digo que não é ortodoxo porque nesse caso, você deveria hospedar seu serviço em um WindowsService ou Console Application que pudesse ser iniciado pelo scheduler do Windows, pois essas são as formas normais de criar serviços que rodam por tempo indeterminado e que executam tarefas de tempos em tempos.

Mas então, porque criar um “serviço” na estrutura do Asp.Net? A resposta é simples: para as opções citadas acima, você precisa ter um servidor dedicado ou um Azure, o que muitas vezes não temos condições de ter, porque o projeto simplesmente não paga uma infraestrutura dessas. Muitas vezes hospedamos nossos sites em servidores baratos que não nos fornece nada mais que o IIS para hospedar nosso site. (Eu acredito que essa seja a realidade da grande maioria dos desenvolvedores).

Ao longo deste post, vou mostrar algumas formas de fazer esses “serviços” funcionarem, e expor os prós e os contras de cada um.

Vamos lá, mãos a obra!

Exemplo 1 – Criando nova Thread no Application Start

A primeira forma que vou demonstrar é bem simples. Vamos criar uma nova Thread na aplicação Asp.Net no momento que ela for iniciada, e essa Thread terá a lógica para manter o serviço sempre ativo e a própria lógica do processamento, o exemplo do código está abaixo:

   1: protected void Application_Start()
   2: {
   3:     AreaRegistration.RegisterAllAreas();
   4:     RegisterGlobalFilters(GlobalFilters.Filters);
   5:     RegisterRoutes(RouteTable.Routes);
   6:     ThreadPool.QueueUserWorkItem(Executar);
   7: }
   8:  
   9: public void Executar(object state)
  10: {
  11:     while (true)
  12:     {
  13:         //Lógica do Serviço
  14:         Thread.Sleep(20000);
  15:     }
  16: }

O código é bem simples. Na linha 6 eu crio uma nova Thread e  a lógica do “serviço” está no método Executar. Todo esse código está no Global.asax.

Prós:

  • Criação bem simples
  • Os usuários do site não são diretamente afetados pelo processo.

Contras:

  • Uma exceção não tratada em uma Thread que não está associada a um Request derrubará todo o processo.
  • Se você estiver em um WebFarm, você poderá criar a Thread em vários servidores, fazendo múltiplas instância do seu processo ser iniciado e mais de uma tarefa pode realizar o mesmo processamento.
  • O AppDomain do seu site pode cair por uma série de motivos e levar junto seu serviço, eventualmente corrompendo dados.

Exemplo 2 – Realizando o processamento em um Request

Essa segunda forma também é simples, porém eu a acho extremamente fraca e feia, mas não deixa de ser uma possibilidade. Ela verifica a necessidade de rodar um serviço em todos os Requests que chegam ao site, neste caso validado no momento de renderizar uma View sempre utilizada. Meu código está no meu arquivo de Layout do Asp.Net MVC (MasterPage se for WebForm):

   1: @functions{
   2:     public bool DeveExecutar(){
   3:         //Lógica para verificar se deve executar ou não, validando algo em cache ou banco de dados, por exemplo
   4:         return true;
   5:     }
   6: }
   7:  
   8: @if (DeveExecutar())
   9: {
  10:     //Lógica do serviço, pode ser em uma nova thread
  11: }

Acima estou mostrando um código em Razor onde eu preciso verificar se está na hora de executar o serviço, podendo fazer isso de diversas formas possíveis, como dito no comentário da linha 3. E também seguindo o comentário da linha 10, para não onerar demais o usuário com o processamento do serviço, você pode iniciar uma Thread nova para realizar sua tarefa.

Prós:

  • Para não falar que é nenhum, é ligeiramente fácil para desenvolver, especialmente em WebForm, que seria apenas codificar uma lógica semelhante à mostrada acima no Page Load da MasterPage.

Contras:

  • Todos os do Exemplo 1.
  • Por depender de Requests, se o seu site não tiver nenhum Request num grande período de tempo, o serviço não será executado.
  • Por interceptar um Request, o usuário poderá perceber que o site está lendo, pois o Response não está voltando tão rápido como deveria.

Exemplo 3 – Realizando o processamento em um Request – Modo 2

O exemplo 3 tem toda a lógica igual ao do Exemplo 3 e praticamente os mesmos Prós e Contras, a única coisa que muda seria a forma de desenvolver. Nesse exemplo, estou tratando o Evento BeginRequest do Global.asax.

   1: protected void Application_BeginRequest()
   2: {
   3:    if (DeveExecutar())
   4:    {
   5:        //Lógica do serviço, pode ser em uma nova thread
   6:    }
   7: }
   8:  
   9: public bool DeveExecutar()
  10: {
  11:    //Lógica para verificar se deve executar ou não, validando algo em cache ou banco de dados, por exemplo
  12:    return true;
  13: }

Esse modo de fazer tem ainda mais um contra:

  • O evento BeginRequest é chamado para qualquer Request, ou seja, para cada Recurso (imagem, js, etc) chamado, não só o Controller, esse método será chamado, o que pode crescer consideravelmente o tempo para carregar uma página.

Até agora vimos que há muito mais problemas do que soluções para criar esse tipo de serviços, vamos mais um pouco a fundo nos problemas, e assim depois poderemos procurar uma solução um pouco mais satisfatória que as definidas acima.

Motivos pelos quais o AppDomain pode cair

No último contra do Exemplo 1, eu comentei que o AppDomain pode cair e causar problemas para sua Thread, vamos ver alguns caso que isso pode acontecer.

O Asp.Net pode derrubar seu AppDomain pelos seguintes motivos, entre outros:

  • Quando você modifica o Web.Config do site o Asp.Net vai realizar o recycle do AppDomain.
  • O IIS vai por conta própria reciclar todo o processo a cada 29 horas, derrubando todos os AppDomain sob ele.
  • Em muitos servidores, é comum o IIS está configurado para derrubar o Application Pool depois de algum período de inatividade, ou seja, se depois de 20 minutos, por exemplo, sem receber um Request, o AppDomain pode ser derrubado.

Nesse caso, você pode estar se perguntando: Esses problemas não podem ocorrer também para Requests normais? O AppDomain não pode cair ou ser reciclado durante a execução de um Request?

Bom, quando o Asp.Net/IIS resolve derrubar/reciclar um AppDomain, ele procura descarregar todos os Requests pendentes e dar um tempo para eles terminarem seus trabalhos. Ele dá esse tempo para os códigos que ele sabe que estão rodando, e normalmente os código que ele “sabe” são os códigos de Requests padrões.

Basicamente, o problema apontado no Exemplo 1 com o AppDomain é que o Asp.Net/IIS não sabe que o seu código está rodando, por não ser um Request normal..

Informando ao Asp.Net que você tem um código especial rodando

Depois de ver tanto problema, vamos começar a estudar uma solução.

Existe uma forma fácil de informar ao Asp.Net sobre o seu código. No namespace  System.Web.Hosting  existe uma classe chamada HostingEnvironment que segundo o MSDN:

Provides application-management functions and application services to a managed application within its application domain

Ou seja, a classe te fornece funções e serviços para gerenciar uma aplicação dentro de um AppDomain.

Dessa classe, precisamos conhecer, para o nosso objetivo, apenas dois métodos: RegisterObject e UnregisterObject.

A função desses métodos é registrar e desregistrar um objeto a lista de “Códigos conhecidos” do Asp.Net.

Esses métodos recebem como parâmetro apenas um objeto que implementa a interface IRegisteredObject, que contém apenas um método, conforme snippet abaixo:

   1: public interface IRegisteredObject
   2: {
   3:     void Stop(bool immediate);
   4: }

Quando o Asp.Net vai derrubar o AppDomain, ele primeiro invoca o método Stop de todos os objetos registrados naquele AppDomain. Em geral ele chama o método duas vezes, primeiro com o parâmetro immediate igual a false, o que te dá um tempo para terminar o seu trabalho, tempo esse que é um total de 30 segundos para todos os objetos registrados terminarem o que estão fazendo, e uma segunda vez é chamado após esses 30 segundos passarem, e desta vez com o parâmetro igual a true, o que seguinifica que você precisa terminar seu trabalho agora, porque seu AppDomain está prestes a sumir do mapa.

O segredo é: Uma vez que o Asp.Net chama o método Stop do seu objeto, você deve impedir que esse método retorne até que seu trabalho tenha terminado. Quando o trabalho terminar, você deve desregistrar seu objeto.

Com essa explicação, vamos ao quarto exemplo:

Exemplo 4 – Criando o serviço em um objeto registrado no Asp.Net

Esse exemplo é melhor que todos os anteriores, pois continua sendo simples, e alguns dos contras são superados:

   1: public class Servico : IRegisteredObject
   2: {
   3:    private readonly object _lock = new object();
   4:    private bool _derrubando;
   5:    public Servico()
   6:    {
   7:        HostingEnvironment.RegisterObject(this);
   8:    }
   9:    public void Stop(bool immediate)
  10:    {
  11:        lock (_lock)
  12:        {
  13:            _derrubando = true;
  14:        }
  15:        HostingEnvironment.UnregisterObject(this);
  16:    }
  17:    public void Executar()
  18:    {
  19:        while (true)
  20:        {
  21:            lock (_lock)
  22:            {
  23:                if (_derrubando)
  24:                {
  25:                    return;
  26:                }
  27:                //Lógica do serviço
  28:            }
  29:            Thread.Sleep(60000);
  30:        }
  31:    }
  32: }

Veja que adicionei um Lock no método Stop e dessa forma impeço que esse método retorne caso esteja no meio de uma execução do método Executar.

No Global.asax eu chamo a minha classe do serviço:

   1: protected void Application_Start()
   2: {
   3:     AreaRegistration.RegisterAllAreas();
   4:     RegisterGlobalFilters(GlobalFilters.Filters);
   5:     RegisterRoutes(RouteTable.Routes);
   6:     ThreadPool.QueueUserWorkItem(state => new Servico());
   7: }

Veja que na linha 6 crio uma Thread nova para o meu objeto, mas agora ele estará registrado no Asp.Net e seu fim não será tão repentino como seria antes.

Agora meus Prós e Contras ficarão da seguinte forma:

Prós:

  • Os usuários do site não são diretamente afetados pelo processo.
  • Terei mais controle do tempo de vida do meu “serviço”

Contras:

  • Uma exceção não tratada em uma Thread que não está associada a um Request derrubará todo o processo.
  • Se você estiver em um WebFarm, você poderá criar a Thread em vários servidores, fazendo multiplas instância do seu processo ser iniciado e mais de uma tarefa pode realizar o mesmo processamento.
  • Para utilizar o RegisterObject é necessário que o site esteja rodando com FullTrust

Essa abordagem seria a mais próxima do ideal. Temos ainda o problema das Exceções, que você deve ter cuidado, e a questão do WebFarm, que você terá que fazer um controle mais manual, mas que foge do objetivo desse post, mas agora temos ao menos mais controle sobre o AppDomain.

Também temos um novo problema, que é a necessidade do FullTrust. Porém, até servidores baratos aceitam FullTrust e muitos frameworks que utilizamos no desenvolvimento também exige FullTrust, então acredito que esse é um problema secundário.

É importante falar ainda que o AppDomain pode cair por outros problemas criticos, e você não será avisado, o que pode ainda corromper seus dados no serviço. Por exemplo, o AppDomain por cair se alguém tropeçar no cabo de força ou se o Windows mostrar uma tela azul, ou por qualquer outro motivo desastroso. Porém, esses riscos você também pode enfrentar em Windows Services normais.

Exemplo 5 – Apenas uma abordagem alternativa ao Exemplo 4

Nesse exemplo venho apenas mostrar um alternativa aos Loops com While(true) que estou utilizando, mas a solução é ainda a do Exemplo 4. Nesse exemplo vou utilizar um Timer no  lugar do While(true). Acredito que essa abordagem seja um pouco mais elegante.

 

   1: using System;
   2: using System.Threading;
   3: namespace Exemplo5
   4: {
   5:     public static class MeuTimer
   6:     {
   7:         private static readonly Timer _timer = new Timer(OnTimerElapsed);
   8:         private static readonly Servico _servico = new Servico();
   9:         public static void Start()
  10:         {
  11:             _timer.Change(TimeSpan.Zero, TimeSpan.FromMilliseconds(60000));
  12:         }
  13:         private static void OnTimerElapsed(object sender)
  14:         {
  15:             _servico.Executar();
  16:         }
  17:     }
  18: }

Só preciso mudar uma linha no meu Global.asax:

   1: protected void Application_Start()
   2: {
   3:     AreaRegistration.RegisterAllAreas();
   4:     RegisterGlobalFilters(GlobalFilters.Filters);
   5:     RegisterRoutes(RouteTable.Routes);
   6:     MeuTimer.Start();
   7: }

Agora na linha 6 eu apenas chamo o método Start do meu Timer, e o meu serviço rodará de tempos em tempos de acordo com o meu Timer.

Conclusão

Vimos algumas alternativas para utilizar serviços na infraestrutura do Asp.Net, mesmo sabendo que essa deva ser a última opção, é bom saber quais alternativas temos. Nitidamente, eu prefiro utilizar a última abordagem, ela é a mais madura de todas.

É importante saber também que essas soluções funcionam tanto em WebForm como em MVC.

Por hoje é isso pessoal.

Segue o fonte dos exemplos:

Valeu!

Obs.: Esse post foi inspirado neste outro post

29. fevereiro 2012 23:32 by Frederico B. Emídio | Comments (0) | Permalink

Sobre mim

Minha Imagem

Meu nome é Frederico Batista Emídio, trabalho com desenvolvimento de sistemas profissionalmente a oito anos, porém, já faço sites pessoais a pelo menos dez anos.

Para saber mais clique aqui.

Páginas

Calendário

<<  novembro 2017  >>
seteququsedo
303112345
6789101112
13141516171819
20212223242526
27282930123
45678910

Visualizar posts em um Calendário
Sigua @fredemidio

MCP Asp.NET