sty
26
2012

ASP.NET : wykonywanie zadania co pewien czas

Często zdarza się, że dostawca udostępniający nam hosting nie daje możliwości zainstalowania na swojej maszynie naszej własnej usługi Windows. A taka usługa przydałaby się nam, np. do okresowego wykonywania jakiejś czynności (przykładowo aktualizowanie statusu jakichś produktów co 5 min). Możemy się oczywiście podpiąć do "normalnego" requestu i wykonać nasze zadanie ale ma to pewne wady. Po pierwsze wymaga dużego ruchu na serwerze (o co np. w godzinach nocnych może być ciężko). Po drugie, rozkład żądań jest niedeterministyczny i trudno tu oczekiwać wykonania zadania o ściśle określonej porze.  I po trzecie, wykonanie jakiegoś "ciężkiego" zadania znacznie spowolni odpowiedź na request który miał nieszczęście zostać nim obdarowany. Pozostają do rozważenia inne sposoby, nad którymi się ostatnio zastanawiałem. Należy tu zaznaczyć, że wszystkie one stoją w pewnej sprzeczności z samą zasadą działania serwera WWW. Jego zadaniem jest w końcu odebranie requesta od klienta, przetworzenia go i wygenerowania odpowiedzi a nie do ciągłego wykonywania jakichś zadań w tle. Na pewno wydajność takiego rozwiązania będzie dużo niższa niż w przypadku dedykowanego serwera aplikacyjnego z zainstalowaną usługą. No ale jeśli takiego serwera nie mamy to pozostaje kilka sposobów na obejście problemu.

1. Wykorzystanie zewnętrznego serwisu do wywoływania naszej strony

Pierwszym sposobem, najbardziej chyba "czystym" jest wykorzystanie zewnętrznego serwisu który można ustawić do periodycznego wywoływania naszej strony/usługi. Można tu wykorzystać np. serwis http://www.cronjobs.org/. Jest to darmowy serwis, w którym możemy ustawić żądanie które będzie wysyłane do naszej strony co określony okres czasu (minimalny interwał to 5 min.). Rozwiązanie dobre, o ile nie potrzebujemy częstszego wykonywania naszego zadania (np. co minutę). Musimy też uważać na generowany ruch bo mogą zacząć nam naliczać opłaty. Rozwiązanie super np. gdy potrzebujemy wykonać archiwizację naszych danych powiedzmy o godz. 2:00 w nocy. 

2. Wykorzystanie mechanizmu WebEvent-ów

Drugim rozwiązaniem, już nie tak ładnym jest wykorzystanie mechanizmu diagnostyki ASP.NET (namespace System.Web.Management). Mamy tu taką ładną klasę WebHeartbeatEvent. Jest to event służący do monitorowania stanu aplikacji WWW co pewien określony okres czasu. Ale kto powiedział, że akurat musi to być tylko monitorowanie :) Możemy sobie napisać własny provider wywoływany przy okazji tego eventu i w nim umieścić logikę naszego zadania. Cała procedura składa się z 2 kroków:

1. Napisanie własnego providera dziedziczącego po WebEventProvider i umieszczenie w nim naszej logiki. Przykładowo:

public class MyWebAppFileLogger : WebEventProvider
    {
        string _logPath;

        public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
        {
            base.Initialize(name, config);
            _logPath = config["logPath"];
        }

        public override void Flush()
        {
        }

        public override void ProcessEvent(WebBaseEvent raisedEvent)
        {
            MyCustomWebEvent ev = raisedEvent as MyCustomWebEvent;
            if (ev != null)
            {
                string fileName = string.Format("{0}{1}{2}.log", DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day);
                using (StreamWriter sw = new StreamWriter(_logPath + "\\" + fileName, true))
                {
                    sw.WriteLine(string.Format("{0} : {1} : {2}", ev.EventTime.ToString(), ev.EventType.ToString(), ev.Message));
                }
            }
        }

        public override void Shutdown()
        {
        }
    }
 

Akurat ten prosty provider tylko loguje swoje wywołanie w pliku loga. Ale oczywiście można tu zaimplementować dowolną logikę.

2. Dopisanie w web.config-u sekcji definiującej nasz provider i podpinającej go do eventu:

<system.web>
	  <healthMonitoring enabled="true" heartbeatInterval="5">
		  <providers>
			  <add name="MyFileLogger" type="MFInterface.Health.MyWebAppFileLogger" logPath="E:\log" />
		  </providers>
		  <rules>
			  <add name="Rule" eventName="Heartbeats" provider="MyFileLogger" profile="Default" minInterval="00:00:01"/>
		  </rules>
	  </healthMonitoring>

W tym przypadku nasz provider będzie wywoływany co 5 sekund. I to by było wszystko, całe rozwiązanie działa chociaż oczywiście łamie w pewien sposób zasadę odpowiedzialności stojącej za mechanizmem WebEvent-ów.

3. Ręczne stworzenie wątku

Ostatnim sposobem jest ręczne odpalenie wątku przy starcie aplikacji WWW. Od razu rodzi to pewne skrzywienie na twarzy: tworzenie własnych wątków w aplikacji WWW to jest to czego zdecydowanie robić nie powinniśmy. Ale uprzedzając słowa krytyki: stworzony wątek będzie tylko 1 i będzie stworzony w Global.asax czyli w kontekście całej aplikacji. Rozwiązanie to ma dużą i niepodważalną zaletę - możemy bardzo dobrze kontrolować stworzony przez nas wątek. Chociażby możemy wykonywać nasze zadanie co 50 ms - przykład (zmiany wykonujemy w Global.asax):

void Application_Start(object sender, EventArgs e)
        {
            Thread t = new Thread(new ThreadStart(DoSomeWork));
            t.Start();

        }

        void DoSomeWork()
        {
            while (true)
            {
                // Zrób jakieś trudne zadanie
                Thread.Sleep(50);
            }
        }

Wiem, nie wygląda to zbyt pięknie - może rodzić też problemy przy zbyt dużym wykorzystaniu zasobów przez nasz wątek. Osobiście najbardziej mimo wszystko przemawia do mnie rozwiązanie nr. 2 - jest niezależne od nikogo z zewnątrz (w przeciwieństwie do 1) i bardziej bezpieczne niż 3. Raczej rzadko zdarza się konieczność wykonywania zadania częściej niż co sekundę.