27/03/2014

SizeLimit, PageSize i dokumentacja

Home

W poście wspomniałem o właściwościach DirectorySearcher.SizeLimit oraz DirectorySearcher.PageSize, których poprawne ustawienie zapewnia, że z bazy danych AD można pobrać więcej obiektów niż ustawiony na serwerze limit. Tym razem chciałbym sprecyzować do czego służą obie właściwości bo moim zdaniem dokumentacja nie jest precyzyjna, co potwierdza zresztą spora liczba pytań w Internecie na ten temat.

Otóż SizeLimit określa maksymalną liczbę obiektów jaka może zostać zwrócona w wyniku wyszukiwania (zero oznacza brak limitu) przy czym limit ustawiony na serwerze ma priorytet. PageSize służy natomiast do skonfigurowania stronicowania i wartość różna od zera je włącza. Na przykład wartość 100 oznacza, że obiekty będą zwracane w paczkach po 100, przy czym .NET jest na tyle miły, że samemu obsługuje doczytywanie kolejnych stron.

Teraz spójrzmy na przykład. W bazie danych AD znajdowało się 1580 obiektów spełniających kryteria wyszukiwania, a limit ustawiony na serwerze wynosił 1500. Poniższa tabelka pokazuje ile obiektów zwróci zapytanie w zależności od ustawień.

SizeLimitPageSizeLiczba obiektów w ADLiczba zwróconych obiektówUwagi
0015801500Brak stronicowania + domyślny limit z serwera
10001580100Brak stronicowania + limit określony przez nas
010015801580Stronicowanie włączone
20010015801580Stronicowanie włączone + limit określony przez nas
1002001580100Stronicowanie włączone + limit określony przez nas

Dwa ostatnie scenariusze są trochę zagadkowe. W obu przypadkach obie właściwości są różne od zera, ale liczba zwróconych obiektów jest inna tj. jeśli SizeLimit > PageSize to z AD pobrano wszystkie dostępne obiekty, a w przeciwnym wypadku tyle ile wynosił SizeLimit. Przypuszczam, że DirectorySearcher działa tak, że pobiera dane póki nie zostanie przekroczony limit. W pierwszym przypadku przy pobieraniu kolejnych stron liczba pobieranych obiektów nie przekracza limitu, a więc udało się odczytać wszystko. W drugim wypadku już przy pobraniu pierwszej strony liczba obiektów przekroczyła limit i dalsze pobieranie zostało zakończone. Pewnie można było to zaimplementować inaczej, ale cóż zrobić i po prostu warto o tym wiedzieć.

18/03/2014

Jak napisać szybki program pobierający dane z AD

Home

Post ten dotyczy tematu efektywnego pobierania danych z Active Directory. Załóżmy, że chcemy pobrać listę użytkowników przy czym interesują nas tylko niektóre właściwości, które ich opisują. Pokażę trzy niewiele różniące się z pozoru sposoby odczytania potrzebnych nam danych. Pozornie ponieważ te trzy podejścia znacząco różnią się wydajnością. W celu zademonstrowania różnic napisałem prostą klasę ADTester. Zawiera ona tylko jedną metodę Run w parametrach, której określamy tryb działania oraz liczbę obiektów do pobrania. Różnica pomiędzy trybami jest następująca:
  • W trybie szybkim (Mode.Fast) dla każdego obiektu z bazy danych Active Directory pobrane zostają tylko wybrane 4 właściwości, a to dzięki zastosowaniu właściwości DirectorySearcher.PropertiesToLoad
  • W trybie normalnym (Mode.Normal) dla każdego obiektu z bazy danych Active Directory pobrane zostają wszystkie dostępne dla danego obiektu właściwości.
  • W trybie wolnym (Mode.Slow) zamiast użyć danych zawartych w obiektach SearchResult korzystam z metody SearchResult.GetDirectoryEntry.
Poniższa tabela pokazuje czas działania programu w ms w zależności od trybu działania oraz liczby obiektów do pobrania:

maxNumberOfObjectsMode.FastMode.NormalMode.Slow
1003634637507
2004076611600
500797221239323
10001353306075935

W najlepszym przypadku tryb prosty jest 2.7 szybszy niż tryb normalny i prawie 60 razy szybszy niż tryb wolny. Różnica jest wręcz powalająca, a wnioski nasuwają się same.
  • Jeśli z góry wiemy jakie właściwości nas interesują to używajmy właściwości DirectorySearcher.PropertiesToLoad.
  • Korzystajmy z danych zwróconych przez klasę DirectorySearcher w postaci obiektów SearchResult.
  • Tylko jeśli to absolutnie konieczne korzystajmy z metody SearchResult.GetDirectoryEntry. Taka potrzeba zachodzi na przykład wtedy jeśli chcemy zmodyfikować dane w AD.
Na koniec jeszcze jedna uwaga. Wyniki czasu działania programu będą się różnić w zależności od tego gdzie znajduje się serwer Active Directory. W moim przypadku znajdował się on poza krajem. Na koniec zamieszczam kod programu do własnych testów:
public class ADTester
{
    public enum Mode { Fast, Normal, Slow }

    public void Run(Mode mode, int maxNumberOfObjects)
    {
        var ldapPath = "YOUR_LDAP_PATH";

        using (var root = new DirectoryEntry(ldapPath))
        {
            using (var searcher = new DirectorySearcher(root)
                    {
                        Filter = "(&(objectClass=user))", SearchScope = SearchScope.Subtree, SizeLimit = maxNumberOfObjects
                    })
            {
                if (mode == Mode.Fast)
                    searcher.PropertiesToLoad.AddRange(new[]{ "displayName","name", "pwdLastSet","userAccountControl" });

                using (SearchResultCollection searchResult = searcher.FindAll())
                {
                    foreach (SearchResult user in searchResult)
                    {
                        if (mode != Mode.Slow)
                        {
                            var displayName = user.Properties["displayName"];
                            ...
                        }
                        else
                        {
                            var entry = user.GetDirectoryEntry();
                            var displayName = entry.Properties["displayName"];
                            ...
                        }
                    }
                }
            }
        }
    }
}

10/03/2014

Czy wyrażenie regularne może zawiesić naszą aplikację?

Home

Rozmawiałem dzisiaj z kumplem z zespołu na temat jednego z zadań i trochę od niechcenie rzuciłem, że można by zastosować tutaj wyrażenie regularne, które dodatkowo byłoby konfigurowalne. Ta z pozoru niewinna uwaga doprowadziła do ciekawej dyskusji. Otóż Tomek stwierdził, że nie jest dobrym pomysłem aby umieszczać wyrażenia regularne w konfiguracji, która może zostać zmieniona przez użytkownika. Dlaczego? W ten sposób umożliwiamy użytkownikowi zawieszenie naszej aplikacji i to nie dlatego, że wyrażenie będzie zawierało błędy składniowe. Aby zrozumieć o co chodzi przyjrzyjmy się takiemu prostemu przykładowi, w którym testuję dwa wyrażenie regularne:
private static void Main(string[] args)
{
   var input1 = "xxxxxxxxxxxxxxxxxxxxxxxxxy";
   var input2 = "xxxxxxxxxxxxxxxxxxxxxxxxx";

   var regex1 = "x+y";
   var regex2 = "(x+)+y";

   TestRegex(regex1, input1);
   TestRegex(regex2, input1);

   TestRegex(regex1, input2);
   TestRegex(regex2, input2);

   Console.WriteLine("Press any key...");
   Console.ReadLine();
}

private static void TestRegex(string r, string input)
{
   var regex = new Regex(r);
   var sw = new Stopwatch();

   sw.Start();

   regex.Match(input);

   sw.Stop();

   Console.WriteLine("{0} ms", sw.ElapsedMilliseconds);
}
Oba użyte wyrażenie są proste. Drugie jest "dziwne" bo po co stosować tutaj konstrukcję grupującą. Oczywiście nie ma to tutaj sensu, ale zrobiłem to aby pokazać ideę problemu. Teraz przejdźmy do setna. Na moim komputerze powyższy program wypisze takie czasy:
30 ticks 0 ms
13 ticks 0 ms
31 ticks 0 ms
21621843 ticks 9246 ms
Różnica jest porażająca. Z pozoru niewinne wyrażenie regularne (x+)+y w przypadku kiedy wejściowy dane do niego nie pasują (w drugim przypadku na końcu ciągu znaków brakuje litery y) jest o duże kilka rzędów wielkości wolniejsze niż wyrażenie x+y Co gorsze im więcej x'ów w danych wejściowych tym te czasy będą gorsze:

Liczba x'ówCzas (ms)
100
144
159
1871
20291
221169
244685
2618741
2874078

Mamy tutaj do czynienia z złożonością wykładniczą! Problem tkwi natomiast w użyciu w drugim wyrażeniu backtracking'u, który powoduje, że silnik wyrażeń regularnych niepotrzebnie wielokrotnie (na)wraca do znalezionych wcześniej dopasowań. Do tego potrzebne są również odpowiednio dobrane dane. Można mówić, że przykład jest tendencyjny (jak to przykład), ale skoro jest to możliwe to kiedyś się zdarzy i dlatego trzeba być świadomym takich rzeczy.

Kilka uwag końcowych:
  • Pewnym obejściem problemu jest wyłączenie backtracking'u dla danej grupy w taki sposób: (?>(x+)+)y
  • Można również określić maksymalny dopuszczalny czas przetwarzania wyrażenia regularnego np.: new Regex(r, RegexOptions.None, new TimeSpan(0,0,0,1))
  • Jeśli to możliwe to zamiast backtracking'u należy stosować tzw. lookahead/lookbehind assertions, które nie nawracają.
  • Problem opisałem na przykładzie .NET, ale może od dotyczyć każdego silnika wyrażeń regularnych, który obsługuje backtracking i który domyślnie z niego korzysta.
  • Jeśli chcecie pogłębić temat to polecam ten artykuł albo ten.

25/02/2014

Dlaczego należy jawnie specyfikować czy kolumna ma akceptować wartości puste czy nie?

Home

Załóżmy, że w procedurze składowanej mamy tabelkę tymczasową:
CREATE TABLE #Temp (Column1 Int, Column2 Varchar(15));
Wszystko działa bez zarzutu, może nawet w środowisku produkcyjnym, aż w pewnym momencie ktoś mówi: Wiesz co dzisiaj Twoja procedura wywaliła się i krzyczy, że kolumna Column1 nie pozwala na wartości NULL. Sprawdzasz błąd na swoim środowisku, ale wszystko działa. Sprawdzasz na innym serwerze, na innej bazie danych i też działa. Co u licha?! Magia czy co?

Jak to zwykle bywa w takich sytuacjach nie magia, ale PEBKAC. Jeśli tworzymy tabelkę tymczasową lub inną i nie podamy jawnie czy kolumna ma akceptować wartości NULL czy nie to domyślnie będzie ona... No właśnie tu tkwi problem, a odpowiedź brzmi to zależy.

Przy standardowych ustawieniach będzie akceptować wartości NULL, ale można to zmienić na poziomie bazy danych, a co gorsza na poziomie sesji użytkownika! Wystarczy, więc mała zmiana i nasz kod przestaje działać. Dlatego dobra praktyka mówi więc aby zawsze jawnie specyfikować czy kolumny mają akceptować puste wartości czy nie.

Co do opcji, które sterują tym zachowanie. Na poziomie bazy danych MSSQL służy do tego komenda:
ALTER DATABASE dbname SET ANSI_NULL_DEFAULT [OFF|ON]
Natomiast ustawienia sesji użytkownika określamy w SQL Server Management Studio:

Tools->Options->Query Execution->SQL Server->ANSI

Lub przy pomocy jednej z dwóch komend, które aby było łatwiej mają dokładnie przeciwne znaczenie i się wykluczają:
SET ANSI_NULL_DFLT_ON ON [OFF|ON]
SET ANSI_NULL_DFLT_OFF ON [OFF|ON]

24/02/2014

WolframAlpha - ponownie

Home

Rzadko zdarza się aby jakieś narzędzie zaskakiwało mnie tak często i tak przyjemnie jak WolframAlpha (pisałem już o nim tutaj lub tutaj). Po prostu kiedy potrzebuję coś policzyć, sprawdzić i szukam programu lub strony, która to dla mnie zrobi to bardzo często wracam właśnie do WolframAlpha.

Tym razem potrzebował czegoś, co wyznaczy mi linię trendu czyli nic dodać nic ująć tylko tzw. prosta regresja liniowa. Okazało się, że w WolframAlpha służy do tego komenda linear fit. Linia prosta to oczywiście bardzo prosty model, który nie zawsze będzie dobrze działać, ale przy tej okazji odkryłem, że WolframAlpha wspiera również bardziej zaawansowane modele, na przykład komenda cubic fit generuje model w postaci równania trzeciego stopnia. Po więcej przykładów odsyłam do tej strony, zachęcam do zapoznania się i własnych poszukiwań.

A do czego takie modele mogą się przydać? Na przykład do predykcji cen, długości życia w zależności od różnych czynników, liczby odwiedzin strony...