.NET Development blog

4Sep/100

ASP.NET sposób na zliczanie użytkowników online – StateServer

Witam Was serdecznie, jak widać od dłuższego czasu nic nie pisałem, gdyż tłumacząc się miałem sporo różnych spraw na głowie :) . Na temat dzisiejszego posta wybrałem problem z którym spotkałem się ostatnio w jednym z moich projektów. Mianowicie sprawa dotyczy zliczania ilości użytkowników, którzy w bieżącej chwili korzystają z naszej aplikacji. System miał już wcześniej zaimplementowaną taka funkcjonalność, ale z pewnych względów (o tym później) przestała działać :) .

Założenia na których bazowało poprzednie rozwiązanie były naprawdę proste, sprowadzały się do obsłużenia kilku zdarzeń aplikacji. Poglądowo wyglądało to mniej więcej tak:

void Application_Start(object sender, EventArgs e)
{
    Application["userCount"] = 0;
}
void Session_Start(object sender, EventArgs e)
{
    Application["userCount"] = (int)Application["userCount"] + 1;
}
void Session_End(object sender, EventArgs e)
{
    Application["userCount"] = (int)Application["userCount"] - 1;
}

Wszystko opierało się na globalnym liczniku trzymanym w obiekcie Aplikacji, inkrementacji podczas rejestrowania nowej sesji oraz dekrementacji w zdarzeniu wygaśnięcia sesji.

Ten prosty mechanizm działał całkiem dobrze... do czasu ;]. Konfiguracja serwera na którym została zainstalowana aplikacja zakładała cykliczny recycling puli aplikacji odbywający się co kilka godzin. Do tej pory dane sesji przechowywane były InProc (domyślne ustawienie), więc podczas każdego restartu wszystkie dane "szlak trafiał" a użytkownicy zostawali wyrzuceni "siłą" z aplikacji.
Aby przeciwdziałać takiemu zachowaniu do trzymania danych sesji użyłem zewnętrznego procesu (stateserver'a), tak więc dane zostaną utracone tylko jeżeli stateserver zostanie zresetowany :) .

<sessionState mode="StateServer" stateConnectionString="tcpip=127.0.0.1:42424" timeout="20">
</sessionState>

Prosty wpis w web.config i sprawa załatwiona.

Wszystko fajnie, użytkownicy już nie są wylogowywani przy każdym restarcie aplikacji super :) . Tylko coś jest nie tak... Zaraz, zaraz czego ta liczba użytkowników tak nagle rośnie...Mały rekonesans i proszę, zdarzenie Session_End nie jest obsługiwane jeżeli tryb sesji jest ustawiony na stateserver. Świetnie, nie mówiąc, że trzeba było dorobić serializację do wszystkich obiektów, które zamierzamy trzymać w sesji to jeszcze licznik przestał działać ;P.

Po pewnym czasie googlowania, zastosowania pewnego mechanizmu opierającego się na httpmodule i timeout'cie, które niestety zawiodło, wpadłem na pewien pomysł zakładający:
- pobranie sessionId klienta przy każdym requescie,
- zapisanie go w bazie danych, wraz z dokładnym czasem request'a,
- jeżeli ten sam sessionId to oczywiście update daty,
- przed zwróceniem danych zapisanych w bazie, usunięcie wszystkich wierszy "przestarzałych" o powiedzmy 10 min,
- zwrócenie reszty danych (no i mamy dane ostatnio aktywnych sesji :) )

No dobra plan nakreślony, więc po pierwsze stworzyłem tabelę [ActiveUser] oraz klasę jej odpowiadającą.

Nie wchodzę w szczegóły implementacji warstw dostępu do danych, biznesowej itd. napisze tylko, że aplikacja wykorzystuje NHibernate w wersji 2.2.

Mając już gdzie składować dane oraz zaimplementowany mechanizm i obiekty biznesowe, możemy zająć się ładowaniem danych do bazy. Tylko gdzie? Ja akurat wykorzystałem zdarzenie aplikacji AcquireRequestState, gdyż tam mamy już dostęp do obiektu sesji.

void Application_AcquireRequestState(Object sender, EventArgs e)
{
    if (HttpContext.Current.Session != null)
    {
        ActiveUser user = MarketBLL.ActiveUserBLL.GetActiveUserBySessionId(HttpContext.Current.Session.SessionID);
        if (user == null)
        {
            user = new ActiveUser();
            user.SessionId = HttpContext.Current.Session.SessionID;
        }
        user.LastActivity = DateTime.Now;
        if (HttpContext.Current.User.Identity != null)
        {
            user.IsAuthenticated = HttpContext.Current.User.Identity.IsAuthenticated;
            user.UserName = HttpContext.Current.User.Identity.Name;
        }
        MarketBLL.ActiveUserBLL.SaveOrUpdate(user);
    }
}

Jak widać kod nie jest skomplikowany zakłada albo update wartości LastActivity albo wrzucenie nowego rekordu do bazy.
Oczywiście przed wybraniem wierszy z bazy w celu zliczenia liczby użytkowników należy usunąć nieaktywnych użytkowników. Akurat u mnie wygląda to mniej więcej tak:

DateTime now = DateTime.Now.AddMinutes(-15);
string queryString = "DELETE FROM dbo.ActiveUser WHERE LastActivity < :now ";
ISQLQuery query = HBManager.Instance.GetSession().CreateSQLQuery(queryString);
query.SetDateTime("now", now);
query.ExecuteUpdate();

Po tym zabiegu mamy w bazie już tylko dane aktywnych użytkowników w ostatnim czasie (u mnie akurat przez ostanie 15 minut). Co z tymi danymi zrobicie pozostawiam już tylko w waszym geście.

Tagged as: No Comments