SPRequest  Udostępnij na: Facebook

Autor: Jakub Gutkowski

Opublikowano: 2011-07-25

Pracując z SharePoint, nie zastanawiamy się na ogół nad tym, jak on został skonstruowany – otwieramy połączenie do SPSite, a następnie prosimy o SPWeb i odczytujemy tytuł witryny. Wszystko wydaje się proste i nieskomplikowane. W rzeczywistości nasze zapytanie o nazwę witryny przechodzi poprzez skomplikowany proces wykorzystujący obiekt SPRequest. Aby było zabawniej, SPRequest wykorzystuje komponent COM, by wykonać nasze zapytanie.

Dokładniej mówiąc, zapytanie o tytuł tworzy nowy obiekt SPRequest (jeżeli nie został on dotąd utworzony dla witryny), rejestrując jego instancję w singletonie SPRequestManager. Następnie nasze zapytanie o tytuł tłumaczone jest na jedną z 400 metod zaimplementowanych w SPRequest, wewnętrznie wykorzystujący SPRequestInternalClass, który implementuje interfejs SPRequestInternal dziedziczący po ISPRequest. Nie będziemy się tu jednak zagłębiać, dlaczego to jest tak, a nie inaczej zrobione, ważne jest że SPRequestInternalClass to tak naprawdę implementacja interfejsu komponentu COM (działa jak proxy).

Lazy Loading. SharePoint robi wszystko, co może, aby opóźnić utworzenie obiektu SPRequest. Jeżeli go jednak tworzymy, otwieramy połączenie do strony za pomocą SPSite – obiekt SPRequest jest tworzony pośrednio przez SPFarm, ale także jest usuwany i nie jest on przypisywany do zmiennej m_Request w obiekcie SPSite. Dopiero odwołanie się np. do nazwy portalu w obiekcie SPSite spowoduje utworzenie obiektu SPRequest. Tak samo dzieje się z witryną, jeżeli otwieramy ją poprzez metodę OpenWeb() z obiektu SPSite. Dopiero gdy odwołamy się np. do  Title, SharePoint utworzy SPRequest – kolejne odpytania o Title jednak nie będą powodowały wykorzystania SPRequest, wartość Title jest przypisywana do zmiennej lokalnej.

SPRequest, mimo że udostępnia ponad 400 metod, sam w sobie korzysta z kolejnej klasy, która ma tyle samo metod. Rolą obiektu SPRequest jest przekazanie wykonania zapytania do komponentu COM oraz zarządzanie wyjątkami, jakie mogą zostać zwrócone przez SPRequestInternalClass i tłumaczenie ich na język ludzi, który nie zawsze jest językiem rozumianym – jednak SPRequest robi co może, aby dało się błąd zrozumieć.

SPRequest również dba o to, aby przy próbie usunięcia obiektu SPRequestInternalClass z pamięci sprawdzić, czy na pewno jakaś operacja wykonana przez użytkownika nie jest właśnie w trakcie wykonywania – prosiliśmy na przykład o tytuł i w trakcie wywoływania polecenia zwrócenia tytułu próbujemy zwolnić zasoby zaalokowane przez komponent COM, którego klasą proxy jest SPRequestinternalClass. W tym konkretnym przypadku możemy otrzymać taki błąd:

You cannot invalidate the SPRequest object while it's in use.

Tak jak z kodu ciężko jest ten problem wywołać, tak w rzeczywistości podczas pracy z SharePoint od czasu do czasu możemy dostać taką informację. Ważne jest to, abyśmy wiedzieli, że jest ona spowodowana próbą zwolnienia zasobów zajmowanych przez SPRequestInternalClass w trakcie wykonywania przez nią metody pobrania danych.

Komponentem COM dla naszym zapytań jest OWSSVR.dll. Jest on sercem SharePointa – bramą do bazy danych. To właśnie przez ten komponent odbywa się cała komunikacja z SQL Server. Jeżeli jednak chcielibyście spróbować wykorzystać sami obiekt SPRequest, to odradzam – choćby z powodu metod, jakie trzeba by było wywołać. Poniższa metoda wykorzystywana jest przez klasę SPWeb do pobrania tytułu witryny oraz dodatkowych informacji na jej temat, jak opis, język itp.:

public void OpenWeb(string bstrUrl, 
    out string pbstrServerRelativeUrl, out string pbstrTitle, 
    out string pbstrDescription, out string pbstrTitleResourceId, 
    out string pbstrDescriptionResourceId, out Guid pguidID, 
    out string pbstrRequestAccessEmail, out uint pwebVersion, 
    out Guid pguidScopeId, out uint pnAuthorID, 
    out uint pnLanguage, out uint pnLocale, 
    out ushort pnTimeZone, out bool bTime24, 
    out short pnCollation, out uint pnCollationLCID, 
    out short pnCalendarType, out short pnAdjustHijriDays, 
    out short pnAltCalendarType, out bool pbShowWeeks, 
    out short pnFirstWeekOfYear, out uint pnFirstDayOfWeek, 
    out short pnWorkDays, out short pnWorkDayStartHour, 
    out short pnWorkDayEndHour, out short pnMeetingCount, 
    out int plFlags, out bool bConnectedToPortal, out string pbstrPortalUrl, 
    out string pbstrPortalName, out int plWebTemplateId, 
    out short pnProvisionConfig, out string pbstrDefaultTheme, 
    out string pbstrDefaultThemeCSSUrl, out string pbstrThemedCssFolderUrl, 
    out string pbstrAlternateCSSUrl, out string pbstrCustomizedCssFileList, 
    out string pbstrCustomJSUrl, out string pbstrAlternateHeaderUrl, 
    out string pbstrMasterUrl, out string pbstrCustomMasterUrl, 
    out string pbstrSiteLogoUrl, out string pbstrSiteLogoDescription, 
    out object pvarUser, out bool pvarIsAuditor, 
    out ulong ppermMask, out bool bUserIsSiteAdmin, 
    out bool bHasUniquePerm, out Guid pguidUserInfoListID, 
    out Guid pguidUniqueNavParent, out int plSiteFlags, 
    out DateTime pdtLastContentChange, out DateTime pdtLastSecurityChange, 
    out string pbstrWelcomePage, out bool pbOverwriteMUICultures, 
    out bool pbMUIEnabled, out string pbstrAlternateMUICultures, 
    out int puiVersion, out short pnClientTag) {}

Jak widać, trzeba podać tych parametrów wiele.

W komponencie możemy znaleźć metody prawie na wszystkie nasze operacje na SharePoint – dodanie elementu, usunięcie strony, aktualizacja elementu, zmiana roli, dodanie pola i wiele, wiele innych. Sama ich liczba jest już przytłaczająca, choć też liczba parametrów nie poprawia sytuacji. Dlatego, mimo wielkiego narzekania na obiekt modelowy SharePoint, możemy być wdzięczni, że został utworzony taki ładny wraper.

Ciekawostki. Pierwszą ciekawostką jest to, że OWSSVR jest także rozszerzeniem ISAPI dla IIS, dzięki czemu możemy go wykorzystać do wykonania zapytania poprzez żądanie HTTP – podstawowy URL dla takiego zapytania wygląda tak:

http://[site_name]/_vti_bin/owssvr.dll

zaś trochę okrojoną dokumentację sposobu wykorzystania można znaleźć tutaj: http://bit.ly/eV0GKb. Jako drugą ciekawostkę można dodać, że właśnie przez OWSSVR Office komunikuje się z SharePoint.

Posiadając podstawowe informacje na temat SPRequest, możemy wyjaśnić dlaczego ten obiekt jest taki ważny i do czego tak naprawdę służy klasa SPRequestManager, która do tej pory została tylko wymieniona z nazwy dwukrotnie.

Ze względu na to, że SPRequest wykorzystuje komponent COM, alokuje on zasoby niezarządzane, które są poza kontrolą środowiska .NET. To znaczy, że jeżeli nie będziemy dobrze zarządzać obiektem SPRequest, możemy spowodować wyciek pamięci, który powtórzony wielokrotnie może doprowadzić do niestabilnego działania całego systemu.

Na przykład, jeżeli utworzymy SPWeb i pobierzemy tytuł strony, zostanie utworzony obiekt SPRequest wraz instancją implementacji komponentu COM, która nie jest zarządzana przez .NET. Zamykając naszą aplikację, .NET wykona za nas czyszczenie obiektów zarządzanych z pamięci – czyli obiekt SPRequest przestanie istnieć, zaś instancja komponentu COM wciąż będzie siedziała w pamięci. W tym momencie nastąpi przeciek. Tak naprawdę nie musimy nawet zamykać aplikacji, wystarczy, że nasz kod wyjdzie poza zasięg danej zmiennej – na przykład opuści funkcję GetTitle():

public void Sample()
{
    var title = GetTitle("http://gutek-dev");
    Console.WriteLine(title);
}

public string GetTitle(string url)
{
    SPSite site = new SPSite(url);
    return site.OpenWeb().Title;
}

Bardzo ciężko jest się zorientować, dlaczego, kiedy, jak i w którym miejscu następuje problem ze zwalnianiem pamięci. Na przykład czy bylibyśmy wstanie powiedzieć, że przeciek nastąpił w kodzie wyżej zamieszczonym, nie wiedząc nic o SPRequest? Moglibyśmy się tylko domyślić, że taki problem istnieje, kiedy nasz serwer nagle przestaje reagować na nasze żądania – mimo wszystko debugowanie tego błędu zajęłoby nam godziny, albo nawet dni. Dlatego Microsoft do SharePoint dodał klasę SPRequestManager.

SPRequestManager jest klasą, której jedynym przeznaczeniem jest śledzenie utworzonych obiektów SPRequest. Każdy utworzony obiekt SPRequest trafia do słownika, którego kluczem jest ID alokacji, i wartością SPRequest w klasie SPRequestManager. Komunikacja pomiędzy SPRequest i SPRequestManager jest dość złożona, ale można śmiało powiedzieć, iż przy zwalnianiu zasobów za pomocą metody Dispose z obiektu SPRequest obiekt ten także usuwa się sam z siebie z SPRequestManager. Zaś SPRequestManager próbuje zwolnić zasoby obiektu SPRequest, który aktualnie poprosił SPRequestManager o usunięcie się. Na szczęście jedna linijka z wartością bool blokuje cykliczne wywoływanie metod Dispose pomiędzy tymi dwoma klasami.

Jeżeli jednak nie wykonamy polecenia Dispose na SPRequest, przy zwalnianiu zasobów zaalokowanych przez obiekt SPRequestManager dostaniemy informację w logu o tym, iż nasz obiekt SPRequest nie został zamknięty zgodnie z najlepszymi praktykami, przez co możemy mieć przeciek pamięci.

Pomimo wysłania takiej wiadomości SPRequestManager spróbuje naprawić za nas ten błąd, jednak jego naprawienie zostanie przesunięte aż do końca cyklu życia obiektu SPRequestManager. Problem polega na tym, że życie obiektu SPRequestManager może być dość długie – dopóki wątek istnieje. Otwieranie więc w pętli obiektów SPWeb i ich niezamykanie poprawne spowoduje dość znaczący przeciek pamięci, który dopiero przy „restarcie” zostanie naprawiony.

Poza tym przy rejestracji obiektu w SPRequestManager –  jeżeli przekroczyliśmy domyślny limit instancji obiektów SPRequest – SPRequestManager poinformuje nas o znacznej liczbie zaalokowanych obiektów SPRequest. Można to odczytać jako „uwaga, istnieje problem, może jest to oczekiwane działanie, ale sprawdź, czy nie masz obiektu do usunięcia z pamięci”.

Wszystkie informacje SPRequestManager logowane są w katalogu 14 HIVE/Logs i mogą być odczytane za pomocą notatnika czy też ULS Viewer.

Zobaczmy więc, jak działa logowanie. Do tego będzie potrzebna nam prosta aplikacja, która w całości jest niepoprawna, ale chcemy uzyskać efekt otrzymania informacji w logu o problemie, a nie napisać  kod tak, aby go nie dostać.

using System;
using Microsoft.SharePoint;

namespace SPRequestSample
{
    public class Program
    {
        static void Main()
        {
            SPSite site = new SPSite("http://gutek-dev");

            for(int i = 0; i < 20; i++)
            {
                SPWeb web = site.OpenWeb();
                Console.WriteLine(web.Title);
            }
        }
    }
}

Spowoduje to wyświetlenie się następujących wpisów w logu (x6):

Potentially excessive number of SPRequest objects (21) currently unreleased on thread 9.  Ensure that this object or its parent (such as an SPWeb or SPSite) is being properly disposed. This object is holding on to a separate native heap. Allocation Id for this object: {0574893B-0630-4DAF-B5BD-B28860F97A80} Stack trace of current allocation:   
 at Microsoft.SharePoint.SPGlobal.CreateSPRequestAndSetIdentity(SPSite site, String name, Boolean bNotGlobalAdminCode, String strUrl, Boolean bNotAddToContext, Byte[] UserToken, String userName, Boolean bIgnoreTokenTimeout, Boolean bAsAnonymous)    
 at Microsoft.SharePoint.SPWeb.InitializeSPRequest()
 at Microsoft.SharePoint.SPWeb.InitWebPublic()
 at Microsoft.SharePoint.SPWeb.get_LanguageCulture() 
 at Microsoft.SharePoint.SPWeb.get_Title() 
 at SPRequestSample.Program.Main()    
 at System.AppDomain._nExecuteAssembly(Assembly assembly, String[] args)  
 at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()    
atSystem.Threading.ExecutionContext.Run(ExecutionContextexecutionContext, ContextCallback callback, Object state)    
 at System.Threading.ThreadHelper.ThreadStart()

An SPRequest object was reclaimed by the garbage collector instead of being explicitly freed.  To avoid wasting system resources, dispose of this object or its parent (such as an SPSite or SPWeb) as soon as you are done using it.  Allocation Id: {0574893B-0630-4DAF-B5BD-B28860F97A80}  To determine where this object was allocated, set Microsoft.SharePoint.Administration.SPWebService.ContentService.CollectSPRequestAllocationCallStacks = true.

Domyślnie informacja w logu pojawi się dopiero wtedy, kiedy liczba obiektów SPRequest przekroczy magiczną liczbę 15, która jest hardcodowana w klasie. Możemy nią jednak operować za pomocą rejestru. Dokładniej działa to tak, że jeżeli nie istnieje wpis LocalSPRequestWarnCount w rejestrze pod kluczem HKLM\SOFTWARE\Microsoft\Shared Tools\Web Server Extensions\HeapSettings, to zostaje przypisana domyślna wartość 15.

Zarówno klucz, jak i jego wartość nie istnieją po instalacji SharePointa – stąd po sześć wiadomości w rejestrze.

W logu możemy też zobaczyć wspomniane wcześnie ID alokacji (GUID) obiektu SPRequest, które przechowywane jest po stronie SPRequestManager. To ID może służyć jako klucz wyszukiwania w logu do znalezienia wszystkich informacji dotyczących danego żądania. Na przykład drugi log nie zawiera stacktrace i jedyne, co możemy zrobić, to przeszukać plik logu w poszukiwaniu ID: {0574893B-0630-4DAF-B5BD-B28860F97A80} – w ten sposób natrafimy na pierwszy wpis ze stacktrace. Jeżeli jednak chcielibyśmy mieć ten sam stacktrace także w ostatnim wpisie, to możemy wykonać następujący skrypt PowerShell:

[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint") > $null 

$svc = [Microsoft.SharePoint.Administration.SPWebService]::ContentService 
$svc.CollectSPRequestAllocationCallStacks = $true 
$svc.Update()

Jeżeli zmienimy kod, aby był on poprawny (używał Dispose):

using System;
using Microsoft.SharePoint;

namespace SPRequestSample
{
    public class Program
    {
        static void Main()
        {
            using(SPSite site = new SPSite("http://gutek-dev"))
            {
                for(int i = 0; i < 20; i++)
                {
                    using(SPWeb web = site.OpenWeb())
                    {
                        Console.WriteLine(web.Title);
                    }
                }
            }
        }
    }
}

to nie dostaniemy żadnych negatywnych logów z SPRequestManager.

Podsumowanie

Trochę zagłębiliśmy się we wnętrze SharePoint, jednak wiedza o tym, co się dzieje w środku, nie jest nam aż tak potrzebna – daje jednak dobry pogląd na to, dlaczego dostajemy pewne informacje w logu i jak sobie z nimi radzić. Dowiedzieliśmy się też, co jest sercem SharePointa prawie od pierwszej wersji, a przez to, że Microsoft stara się utrzymać kompatybilność wstecz, pewne rzeczy nie są naprawiane/poprawiane – skoro działa, to po co to zmieniać? Ma to także wpływ na aplikację Office, które wykorzystują rozszerzenie ISAPI, komunikując się z SharePoint – zmiana sygnatury metody mogłaby spowodować, że nasz Word odmówiłby współpracy z SPS.

Wiemy też, jak możemy podszlifować log za pomocą dwóch ustawień – liczby obiektów SPRequest, jakie mogą być utworzone bez ostrzeżenia o przekroczeniu limitu, oraz jak włączyć logowanie stacktrace dla informacji, w którym miejscu został zaalokowany SPRequest.