StronicowanieW poprzednim wpisie udowodniłem ,że nawet na moim obecnym super sprzęcie i systemie 64-bitowym nie jestem wstanie przesłać 2.000.000 rekordów w WCF za jednym zamachem. W tym wpisie pokaże jak za pomocą “stronicowania” (paginacji) można rzeczywiście przesłać ,aż tyle informacji.

Oto drugi wpis z cyklu “WCF Big Large Data”.

Do kontraktu WCF, czyli interfejsu została dodana metoda “GetOnePage()”. Będzie ona przyjmować dwa parametry. Numer rekordu, od którego mają być podawane rekordy oraz ich liczbę.

WCF Big DataStronicowanie WCF

Pozostałem metody tworzą dwu milionową kolekcję jak i próbują  wysłać  kolekcje jako całość .Opisałem je w poprzednim wpisie.

[ServiceContract]
public interface IDataService
{
    [OperationContract]
    List<ImaginaryTableEntry> GetImaginaryTableEntries();

    [OperationContract]
    void CreateImaginaryData();

    [OperationContract]
    List<ImaginaryTableEntry> GetOnePage(int pStart, int pLength);
}

Kluczem do szybkość rozwiązania jest kod po stronie serwera.

Rekordy są pobierane z pliku CSV. Patrząc na kod metody łatwo zauważyć ,że im dalej szukamy w pliku CSV tym wolniej będzie otrzymywać wyniki. Kod zawsze skanuje plik od początku.

Jest to istotne, ponieważ gdybyśmy korzystali z baz danych i z tabeli która ma 2.000.000 rekordów i to jeszcze po indeksowanych rekordów zapewne wyniki byłby zwracany dużo szybciej. Oto dlaczego firmy przechowują dane w bazach ,a nie w plikach tekstowych. Jest to jednak tylko przykład, ponieważ stworzenie tabeli  z 2.000.000 rekordów jest trochę kłopotliwe.

public List<ImaginaryTableEntry> GetOnePage(int pStart, int pLength)
{
    string path = System.Configuration.ConfigurationManager.AppSettings["DataPath"];

    try
    {
        List<ImaginaryTableEntry> result = new List<ImaginaryTableEntry>();
        StreamReader sr;
        string line;
        string[] lineSplit;
        int i = 0;
        int remaining = pLength;
        sr = File.OpenText(path);
        while (!sr.EndOfStream && remaining > 0)
        {
            line = sr.ReadLine();
            if (i >= pStart)
            {
                lineSplit = line.Split(myInnerSeparator, StringSplitOptions.None);
                result.Add(new ImaginaryTableEntry()
                {
                    ImaginaryString = lineSplit[0],
                    ImaginaryBiggerString = lineSplit[1],
                    ImaginaryReturnValue = lineSplit[2]
                });
                remaining--;
            }
            i++;
        }
        return result;
    }
    catch (Exception ex)
    {
    }

    return null;
}

Wypadku błędu jest zwracany null (to akurat trochę niestosowane). Gdy kolekcja nie ma już dalej rekordów wtedy klient otrzymuje listę bez elementów.

Wadą tego rozwiązania jest brak transakcji. Jeśli kolekcja ulegał zmianie w trakcie  pobierania jej w częściach wtedy otrzymamy błędne dane. Jeśli rekord został skasowany w trakcie próby pobierania to otrzymamy w którymś miejscu duplikat.

Rozwiązanie jest proste ,ale w następnym wpisie spróbuje stworzyć rozwiązanie, które będzie umożliwiało modyfikacje tych 2 milionów danych ,a wtedy trzeba będzie wprowadzić terminy “sesji” i “transakcji”.

Jak sprawa wygląda po stronie klienta.

Mamy już C# 5.0 i rozwiązanie async i await. Postanowiłem przedstawić rozwiązanie klienta w starym stylu (Event Based) i nowym “async i await”.

Console Event Based Klient WCF

Do testowanie metody stronicowania postanowiłem użyć zwykłej konsoli. Konsola przy uruchomieniu od razu odpala metodę w WCF “GetOnePage()”.

Zanim jednak przejdę do omawiania kodu muszę coś trochę powiedzieć o konfiguracji referencji usługi WCF w Visual Studio 2012.

Configure Service WCF

Domyślnie Visual Studio 2012 generuje kod oparty na klasach “Task”. W takim wypadku używamy słów kluczowych async i await aby odpalać metody usługi sieciowej.

Aby to zmienić w konfiguracji trzeba zaznaczyć “Generate asynchronous operations”. Teraz otrzymamy kod opartych na zdarzeniach. W kodzie tym  najpierw wywołujemy metodę ,a potem wynik obsługujemy w zdarzeniu X-Completed.

Configure Service  Reference WCF

Dla aplikacji konsolowej typ kolekcji zwracanej jest domyślnie tablicą zmieniłem to na listę generyczną.

W aplikacji konsolowej zadeklarowałem parę stałych. Obiekt listy “myList” pod koniec będzie przechowywał 2.000.000 rekordów.

Klasa StopWatch służy do pomiaru czasu. Jej działanie opisałem w tutaj.

W metodzie głównej uruchamiam zegar ,a potem wykonuje metodę “GetOnePageEventBasedVersion”. Po pobraniu wyświetlę wyniki działania w konsoli w metodzie “ShowStats”. Konsola jest przetrzymywana  poleceniem “ReadLine”, czyli jeśli wciśniesz enter w trakcie pobierania aplikacja konsolowa się wyłączy. Trzeba wcisnąć enter, gdy pobieranie zostało już zakończone.

private static Stopwatch stopWatch = new Stopwatch();
private static List<ImaginaryTableEntry> myList;
private static int myPageSize = 100000;
private static DataServiceRef.DataServiceClient client = new DataServiceRef.DataServiceClient();

static void Main(string[] args)
{
    myList = new List<ImaginaryTableEntry>();
    stopWatch.Start();

    GetOnePageEventBasedVersion(1, myPageSize);
    ShowStats();
}

Metoda GetOnePageEventBasedVersion podpina zdarzenie “Completed” po czym odpala metodę sieciową “GetOnePage”.

public  static void GetOnePageEventBasedVersion(int start, int end)
{
    client.GetOnePageCompleted += client_GetOnePageCompleted;
    client.GetOnePageAsync(start, end);
}

Na początku zostanie pobranych pierwsze 100.000 rekordów.Następne zostaną pobrane dzięki magii rekurencji.

Rekurencja musi mieć swój koniec dlatego przy każdym wywołaniu sprawdzamy czy rezultat nie jest pusty. Jeśli jest to znaczy ,że nie mamy już czego pobierać.

static void client_GetOnePageCompleted(object sender, GetOnePageCompletedEventArgs e)
{
    if (e.Error == null)
    {
        bool done = e.Result == null || e.Result.Count < myPageSize;
        if (e.Result != null)
        {
            foreach (ImaginaryTableEntry t in e.Result)
            {
                myList.Add(t);
            }
        }

        if (done)
        {
            Console.WriteLine("\tSkończone");
        }
        else
        {
            Console.WriteLine(myList.Count);
            client.GetOnePageAsync(myList.Count, myPageSize);
        }
    }
    else
    {
        Console.WriteLine("Coś poszło nie tak");
        Console.WriteLine(e.Error.Message);
        Console.ReadLine();
    }
}

Oto jak konsola działa w praktyce. Gif jest przyspieszony o 1000%. Jak widać ostatnie wywoływania trwają dużo dłużej. Tak ja mówiłem plik CSV jest zawsze odczytywany od początku do ostatniego interesującego nas elementu. Konsola wyświetla ostatni i środkowy element kolekcji udowadniając w ten sposób ,że rzeczywiście udało nam się pobrać 2.000.000 rekordów.

gif for wcf bif daat 3

2.000.000 rekordów to całkiem dużo danych dlatego klient zapewne w Windowsie 32-bitowym wywaliłbym by błąd “Out of Memory Exception”. Mnie się udało przypadkiem wywołać wyjątek “AggregateException”, ponieważ uruchomione przeglądarki ze 100 zakładkami jedzą dużo ramu.

bajty

Jak rekurencyjne wywołanie metody WCF wygląda w async i await?

Console Async Await Klient WCF

W metodzie głównej nic się nie zmienia.

static void Main(string[] args)
{
    myList = new List<ImaginaryTableEntry>();
    stopWatch.Start();
    GetOnePageAsyncAwaitVersion(1, myPageSize);
    ShowStats();
}

Dzięki słowom “async” i “await” nie mamy rozbicia kodu na zdarzenie X-Completed. Kod wygląda podobnie. Tym razem rekurencyjnie jest wywoływana metoda konsolowa ,a nie metoda z usługi sieciowej.

public async static void GetOnePageAsyncAwaitVersion(int start, int end)
{
    var k = client.GetOnePageAsync(start, end);

    await Task.WhenAny(k);

    if (k.IsFaulted == false)
    {
        bool done = k.Result == null || k.Result.Count < myPageSize;
        if (k.Result != null)
        {
            foreach (ImaginaryTableEntry t in k.Result)
            {
                myList.Add(t);
            }
        }

        if (done)
        {
            Console.WriteLine("\tSkończone");
            stopWatch.Stop();
        }
        else
        {
            Console.WriteLine(myList.Count);
            GetOnePageAsyncAwaitVersion(myList.Count, myPageSize);
        }
    }
    else
    {
        Console.WriteLine("Coś poszło nie tak");
        if (k.Exception != null) Console.WriteLine(k.Exception.Message);
        Console.ReadLine();
    }
}

Do zobaczenie w następnym wpisie. Na potrzeby sesji i transakcji chyba będę musiał najpierw zbudować porządnego klienta w WPF lub Silverlight.