Code Review: Enum.ToString()–wydajność

Dzisiaj zaciekawiła mnie informacje, znaleziona w sieci, że ToString() na enum jest bardzo powolne. Postanowiłem to sprawdzić samemu. Załóżmy,  że mamy:

public enum Month
{
   January,
   February,
   March,
   April,
   May,
   June,
   July,
   August,
   September,
   October,
   November,
   December
};

Prosty benchmark, może wyglądać następująco:

int n = 100000;

Stopwatch stopwatch = Stopwatch.StartNew();
string text = null;

for (int i = 0; i < n; i++)
{
 var month = (Month)(i % 12);
 text = month.ToString();
}

Console.WriteLine(stopwatch.ElapsedTicks);

Wynik:

image

Taki wynik za wiele nam nie mówi. Spróbujmy napisać drugi przykład, który zamiast enum.ToString(), będzie korzystał z tablicy napisów tzn.:

string[] names = new[]
       {
           "January",
           "February",
           "March",
           "April",
           "May",
           "June",
           "July",
           "August",
           "September",
           "October",
           "November",
           "December"
       };

Wartość enum’a może zostać przeskalowana na indeks w tej tablicy:

stopwatch = Stopwatch.StartNew();
text = null;

for (int i = 0; i < n; i++)
{
 var month = (Month) (i%12);
 text = names[(int)month];                    
}

Console.WriteLine(stopwatch.ElapsedTicks);

Uruchamiając oba benchmarki, zobaczymy:

image

Różnica podobno jest na tyle duża, że nie można tego traktować jako mikro-optymalizacji. Jeśli mamy kod w jakieś pętli (serwer), to stanowi to zwykłe marnowanie zasobów.

No dobra, to odpowiedzmy sobie na pytanie, skąd taka duża różnica? Zaglądamy oczywiście do IL:

// loop start (head: IL_0025)
IL_0010: ldloc.2
IL_0011: ldc.i4.s 12
IL_0013: rem
IL_0014: stloc.3
IL_0015: ldloc.3
IL_0016: box ConsoleApplication2.Month
IL_001b: callvirt instance string [mscorlib]System.Object::ToString()
IL_0020: pop
IL_0021: ldloc.2
IL_0022: ldc.i4.1
IL_0023: add
IL_0024: stloc.2

IL_0025: ldloc.2
IL_0026: ldloc.0
IL_0027: blt.s IL_0010
// end loop

Widzimy, że ToString jest metodą wirtualną (należącą do Object) a enum jest value type. Oznacza to, że będziemy mieli boxing, co widać po IL. Dla porównania drugie rozwiązanie nie zawiera żadnego boxingu:

// loop start (head: IL_00ce)
IL_00bb: ldloc.s i
IL_00bd: ldc.i4.s 12
IL_00bf: rem
IL_00c0: stloc.s month
IL_00c2: ldloc.s names
IL_00c4: ldloc.s month
IL_00c6: ldelem.ref
IL_00c7: pop
IL_00c8: ldloc.s i
IL_00ca: ldc.i4.1
IL_00cb: add
IL_00cc: stloc.s i

IL_00ce: ldloc.s i
IL_00d0: ldloc.0
IL_00d1: blt.s IL_00bb
// end loop

Niestety to nie wszystko. Zagłębmy się dalej do Enum.ToString():

//Enum.ToString()
public override string ToString()
{
    return Enum.InternalFormat((RuntimeType)base.GetType(), this.GetValue());
}

Następnie do Enum.InternalFormat:

// System.Enum
private static string InternalFormat(RuntimeType eT, object value)
{
    if (eT.IsDefined(typeof(FlagsAttribute), false))
    {
        return Enum.InternalFlagsFormat(eT, value);
    }
    string name = Enum.GetName(eT, value);
    if (name == null)
    {
        return value.ToString();
    }
    return name;
}

W zależności czy Enum jest opatrzony atrybutem Flags, mamy dwie różne implementacje. O flagach już kiedyś pisałem (zapraszam do lektury).

W naszym przypadku, przechodzimy do Enum.GetName():

public static string GetName(Type enumType, object value)
{
    if (enumType == null)
    {
        throw new ArgumentNullException("enumType");
    }
    return enumType.GetEnumName(value);
}

Nic tu ciekawego nie ma więc zaglądamy teraz do GetEnumName:

// System.Type
/// <summary>Returns the name of the constant that has the specified value, for the current enumeration type.</summary>
/// <returns>The name of the member of the current enumeration type that has the specified value, or null if no such constant is found.</returns>
/// <param name="value">The value whose name is to be retrieved.</param>
/// <exception cref="T:System.ArgumentException">The current type is not an enumeration.-or-<paramref name="value" /> is neither of the current type nor does it have the same underlying type as the current type.</exception>
/// <exception cref="T:System.ArgumentNullException">
///   <paramref name="value" /> is null.</exception>
public virtual string GetEnumName(object value)
{
    if (value == null)
    {
        throw new ArgumentNullException("value");
    }
    if (!this.IsEnum)
    {
        throw new ArgumentException(Environment.GetResourceString("Arg_MustBeEnum"), "enumType");
    }
    Type type = value.GetType();
    if (!type.IsEnum && !Type.IsIntegerType(type))
    {
        throw new ArgumentException(Environment.GetResourceString("Arg_MustBeEnumBaseTypeOrEnum"), "value");
    }
    Array enumRawConstantValues = this.GetEnumRawConstantValues();
    int num = Type.BinarySearch(enumRawConstantValues, value);
    if (num >= 0)
    {
        string[] enumNames = this.GetEnumNames();
        return enumNames[num];
    }
    return null;
}

Widzimy znów masę if’ow. Chcemy się jednak bliżej przyjrzeć BinarySearch oraz GetEnumNames. Pierwsza z nich daje nam indeks, a druga nazwy enumów.  GetEnumNames wykonuje kilka ifów i wywoła GetEnumData:

// System.Type
private void GetEnumData(out string[] enumNames, out Array enumValues)
{
    FieldInfo[] fields = this.GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
    object[] array = new object[fields.Length];
    string[] array2 = new string[fields.Length];
    for (int i = 0; i < fields.Length; i++)
    {
        array2[i] = fields[i].Name;
        array[i] = fields[i].GetRawConstantValue();
    }
    IComparer @default = Comparer.Default;
    for (int j = 1; j < array.Length; j++)
    {
        int num = j;
        string text = array2[j];
        object obj = array[j];
        bool flag = false;
        while (@default.Compare(array[num - 1], obj) > 0)
        {
            array2[num] = array2[num - 1];
            array[num] = array[num - 1];
            num--;
            flag = true;
            if (num == 0)
            {
                break;
            }
        }
        if (flag)
        {
            array2[num] = text;
            array[num] = obj;
        }
    }
    enumNames = array2;
    enumValues = array;
}

Pierwsza rzecz jaką tu widzimy to refleksja, o której wiemy, że nie jest szybka i spowalnia kod. Widzimy, metoda jest bardzo uniwersalna, ale to skutkuje, że jest wolna dla najprostszych przykładów (brak flag i przerw między enumami). Ponadto, drugi parametr wyjściowy, kompletnie jest niepotrzebny dla naszego przypadku (w zasadzie jest ignorowany przez GetEnumNames):

public virtual string[] GetEnumNames()
{
    if (!this.IsEnum)
    {
        throw new ArgumentException(Environment.GetResourceString("Arg_MustBeEnum"), "enumType");
    }
    string[] result;
    Array array;
    this.GetEnumData(out result, out array);
    return result;
}

BinarySearch znajduje daną wartość korzystając oczywiście z przeszukiwania binarnego:

ulong[] array2 = new ulong[array.Length];
for (int i = 0; i < array.Length; i++)
{
array2[i] = Enum.ToUInt64(array.GetValue(i));
}
ulong value2 = Enum.ToUInt64(value);
return Array.BinarySearch<ulong>(array2, value2);

Widzimy zatem, że kod wykonuje bardzo dużo rzeczy (refleksja, warunki, binarne przeszukiwanie, znajdowanie nazwy, boxing). W drugim rozwiązaniu, nie jest potrzebna żadna pętla aby znaleźć konkretną wartość (w końcu już ją mamy).

Warto mieć  uwadze  powyższe uwagi, wrzucając Enum.ToString gdzieś na serwer. Często przecież ma to charakter niejawny tzn.:

Console.WriteLine(month);
// powyzsza linia jest rownowazna z:
Console.WriteLine(month.ToString());

Visual Studio 2015: Smart Unit Tests

Jeśli jeszcze nie ściągneliście VS 2015, to zapraszam:

http://www.visualstudio.com/en-us/downloads/visual-studio-2015-downloads-vs.aspx

Smart Unit Tests to sposób automatycznego wygenerowania scenariuszy dla testów jednostkowych. Wiele o tym już na blogu jak i w artykułach na MSDN pisałem, a konkretnie o Pex & moles. Chciałbym dzisiaj jednak zaprezentować wbudowaną funkcję w VS 2015. Nie wymaga ona instalacji zewnętrznych narzędzi.

Na początku od razu poważna wada.. Póki co, wspierany jest wyłącznie MSTest. Mam nadzieję, że zmieni się to w oficjalnym wydaniu bo inaczej większość osób nie będzie mogła z tego korzystać.

Załóżmy, że zaimplementowaliśmy już jakąś metodę i chcemy wygenerować test:

public double AnyMethod(int a,int b,double c,string text)
{
  if (a == 3)
      return 1;
  else if (b == a && a == 1)
      return 2;
  else if (b == (int)c && a == 6 && text == "Hello")
      return 3;
  else if (Int32.Parse(text) == a)
      return 4;
  else if (c == a + b)
      return 5;

  return 6;
}

Smart Unit tests postara się wygenerować tak dane wejściowe, aby każda gałąź została pokryta. Zaznaczamy zatem metodę i z menu kontekstowego wybieramy Smart Unit Test:

image

Zostanie wyświetlone następujące okno ze scenariuszami:

image

Widzimy po kolumnie result, że wszystkie scenariusze zostały wygenerowane – wartości od 1 do 6 (wszystkie możliwe ścieżki).

Ponadto, część scenariuszy generuje wyjątki. Wiąże się to z tym, że nie można przekazać tekstu do parametru text i potem wywoływać Int32.Parse. Klikając na każdym z nich, po prawej stronie wyświetli się wygenerowany test:

image

Możemy dowolny scenariusz zapisać do pliku np.:

public partial class SampleTest
{
   [TestMethod]
   [PexGeneratedBy(typeof(SampleTest))]
   public void AnyMethod898()
   {
       double d;
       Sample s0 = new Sample();
       d = this.AnyMethod(s0, 6, 0, 0, "Hello");
       Assert.AreEqual<double>(3, d);
       Assert.IsNotNull((object)s0);
   }
}

Oczywiście możemy wszystkie pozycje zaznaczyć i zapisać.

Powyższe okno również zawiera uwagi i wskazówki:

image

Warto zawsze przejrzeć listę bo czasami możemy dowiedzieć się czegoś, czego nie spodziewaliśmy się po naszym kodzie.

Zmodyfikujmy teraz trochę AnyMethod, aby sprawdzał, że text jest zawsze liczbą:

public double AnyMethod(int a, int b, double c, string text)
{
  if (text == null || !text.All(x => char.IsDigit(x)))
      throw new ArgumentException();

  if (a == 3)
      return 1;
  else if (b == a && a == 1)
      return 2;
  else if (b == (int)c && a == 6 && text == "Hello")
      return 3;
  else if (Int32.Parse(text) == a)
      return 4;
  else if (c == a + b)
      return 5;

  return 6;
}

Po ponownym wygenerowaniu scenariuszy zobaczymy:

image

Smart Unti Test rozpoznał sam, że wyrzucenie ArgumentException jest spodziewanym wynikiem, gdy przekazujemy tekst zamiast liczby. Niestety nie uwzględniliśmy przypadku, gdy Text jest pusty, stąd 4 testy zakończone błędem. Zmodyfikujmy kod i odpalmy analizę ponownie:

public double AnyMethod(int a, int b, double c, string text)
{
  if (string.IsNullOrEmpty(text) || !text.All(x => char.IsDigit(x)))
      throw new ArgumentException();

  if (a == 3)
      return 1;
  else if (b == a && a == 1)
      return 2;
  else if (b == (int)c && a == 6 && text == "Hello")
      return 3;
  else if (Int32.Parse(text) == a)
      return 4;
  else if (c == a + b)
      return 5;

  return 6;
}

image

Jak widzimy, wszystkie gałęzie zostały pokryte, włączając te, które wyrzucają ArgumentException.

Podsumowując… Przede wszystkim, należy pamiętać, że pokrycie kodu 100% nie znaczy, że wszystko zostało przetestowane. Sprawdza to, czy każda gałąź zostanie wywołania, a nie czy każda kombinacja gałęzi jest pokryta.

Kolejna wątpliwość to bardzo popularne dzisiaj TDD. Tak naprawdę, kod powinien być pokryty już testami, jak kończymy implementację czegoś.

Dla mnie największą jednak wadą jest brak wsparcia dla nUnit. Gdybym mógł z niego korzystać, to nawet w połączeniu z TDD może to się przydać. Zawsze mamy legacy kod i możemy popełnić jakiś błąd więc Smart Unit Test byłoby fajnym sposobem na weryfikację naszego kodu. Innymi słowy, nie korzystałbym z tego jako coś bazowego dla moich testów, ale wyłącznie uzupełniającego.

Co prawda, nie testowałem tego jeszcze, ale podobno Microsoft Pex oraz Code Contracts współpracują ze sobą, co oznacza, że moglibyśmy generować bardziej trafne scenariusze użycia za pomocą Smart Unit Test.

C# 6.0: String interpolation

Dziś znów o nowościach w C# 6.0. Tym razem o interpolacji string’a, który usprawni jego aktualną implementację. Należy, zaznaczyć, że ostateczna wersja nie jest jeszcze znana i wszystkie wpisy, które poświęciłem C# 6.0 mogą zostać zmienione jeszcze w oficjalnej wersji języka.

Ale po kolei… Najpierw polecam ściągnąć VS 2015, jeśli jeszcze tego nie zrobiliście. Co to jest string interpolation?Jak to w tej branży bywa, jest to skomplikowane słowo na bardzo prostą rzecz. W aktualnych wersjach języka często korzystamy z string.Format:


string firstName =...
string lastName = ...
string text = string.Format("Witaj {0} {1}", firstName, lastName);

Innymi słowy, interpolowanie stringa polega na zastąpieniu pewnych miejsc ({0} – “placeholdders”) konkretnymi wartościami.

Co jest złego w string.Format? Dla skomplikowanych wyrażeń,  jest to po prostu mało wygodne w użyciu.

Na przykład, stara wersja:

String.Format("{0}, {1}!", hello, world)

Jest równoznaczna, z napisaniem w c# 6.0 tego:

$"{hello}, {world}!"

Składnia o wiele krótsza. To dopiero jednak początek. Kolejny przykład (z oficjalnej dokumentacji na roslyn codeplex):

Będzie równoznaczny z :

$"Name = {myName}, hours = {DateTime.Now:hh}"

String.Format("Name = {0}, hours = {1:hh}", myName, DateTime.Now)

Placeholder zatem zamiast zawierać indeks zmiennej, stanowi po prostu jej nazwę.

Warto wspomnieć, że w preview na razie dostępna jest wyłącznie taka składnia:

"\{hello}, \{world}!"

Z tego co wiem, w oficjalnym wydaniu będzie $, ale to jeszcze nie jest pewne.

Składnia nie ogranicza się wyłącznie do wstawiania wartości z określonym formatowaniem. Można wykonać dowolne wyrażenie np.:

int a = 5;
Console.WriteLine("Value is \{(a>0?"positive":"negative")}");

IEvent, ICommand, IMessage – część VIII

W poprzednich wpisach używałem dwóch sposobów deklaracji wiadomości:

public class PersonAdded : IEvent
{
   public Guid PersonId { get; set; }
}
public class AddPerson:IMessage
{
   public Guid PersonId { get; set; }
   public string FirstName { get; set; }        
   public string LastName { get; set; }
}

Istnieje jeszcze trzeci interfejs, ICommand:

public class AddPerson:ICommand
{
   public Guid PersonId { get; set; }
   public string FirstName { get; set; }        
   public string LastName { get; set; }
}

Wszystkie one służą do definicji wiadomości. Jaka jest więc różnica? IMessage jest najbardziej ogólny. Wiadomość zdefiniowana za pomocą IMessage może być zarówno zdarzeniem jak i komendą.

Komendy nie mogą być publikowane (Publish) i subskrybowane (Subscribe). Mogą z kolei być wysłane za pomocą Bus.Send jak to widzieliśmy w jednym z pierwszych wpisów.

Z kolei IEvent (zdarzenia), analogicznie mogą być publikowane i subskrybowane, ale nie można ich wysyłać za pomocą Bus.Send.

IMessage jest przydatny dla Bus.Reply, gdzie zwrotna wiadomość może być zarówno komendą jak i zdarzeniem.

Publikacja i subskrypcja zdarzeń (przykład), część VII

Dziś o kolejnym, moim zdaniem jednym z najważniejszych elementów programowania rozproszonego, a mianowicie o zdarzeniach. Jak w każdej analogicznej technologii, mamy zaimplementowany wzorzec obserwator. Każdy węzeł może nasłuchiwać konkretnej wiadomości i jak tylko zostanie ona opublikowana, wszyscy subskrybenci zostaną poinformowani.

Zacznijmy od przykładu. W poprzednich wpisach stworzyliśmy Example.API oraz klienta. Dzisiaj rozszerzymy solucję o dwa nowe projekty: Subscriber1 oraz Subscriber2. Skorzystamy z wiadomości AddPerson oraz dodamy pierwsze zdarzenie PersonAdded:

public class PersonAdded : IEvent
{
   public Guid PersonId { get; set; }
}
public class AddPerson:IMessage
{
   public Guid PersonId { get; set; }
   public string FirstName { get; set; }        
   public string LastName { get; set; }
}

Zamiast IMessage, implementujemy IEvent. To właśnie te zdarzenie będziemy publikować.  Następnie modyfikujemy AddPersonHandler z Example.API:

class AddPersonHandler : IHandleMessages<AddPerson>
{
   private readonly IBus _bus;

   public AddPersonHandler(IBus bus)
   {
       _bus = bus;
   }

   public void Handle(AddPerson message)
   {
       Console.WriteLine("Adding person. {0} {1}",message.FirstName,message.LastName);
       _bus.Publish(new PersonAdded(){PersonId = message.PersonId});
   }
}

Jak widać publikacja zdarzeń jest bardzo prosta. Kolejny etap to implementacja subskrybentów. Musimy stworzyć dwa nowe projekty oraz standardowo zainstalować nServiceBus, zainicjalizować EndPointCofig. Niczym to się nie różni od Example.API więc nie będę tego powtarzał. W pliku konfiguracyjnym jednak, musimy określić jakie wiadomości będą nasłuchiwane. Skorzystamy z konfiguracji klienta i powinno to wyglądać następująco:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="MessageForwardingInCaseOfFaultConfig" type="NServiceBus.Config.MessageForwardingInCaseOfFaultConfig, NServiceBus.Core" />
    <section name="UnicastBusConfig" type="NServiceBus.Config.UnicastBusConfig, NServiceBus.Core" />
  </configSections>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  <MessageForwardingInCaseOfFaultConfig ErrorQueue="error" />
  <UnicastBusConfig>
    <MessageEndpointMappings>
      <add Assembly="Example.Contracts" Endpoint="Example.API@localhost" />
    </MessageEndpointMappings>
  </UnicastBusConfig>
</configuration>

Obsługa zdarzeń niczym nie różni się od obsługi innych wiadomości zatem handler wygląda tak:

public class AddPersonEventHandler:IHandleMessages<PersonAdded>
{
   public void Handle(PersonAdded message)
   {                        
       Console.WriteLine("Subscriber 1. Person added: {0}",message.PersonId);
   }
}

Oczywiście analogicznie sytuacja wygląda dla innych subskrybentów.  Możliwa jest również ręczna subskrypcja za pomocą:

Bus.Subscribe<PersonAdded>();    
Bus.Unsubscribe<PersonAdded>();

Jeśli chcemy zablokować autosubskrypcję to:

configuration.DisableFeature<AutoSubscribe>();

W praktyce jednak wygodniej jest korzystać z pliku konfiguracyjnego + autosubskrypcji, jak to zostało pokazane wcześniej.

Na początek taki prosty przykład wystarczy. W przyszłym wpisie, pokażę jak to działa od strony wewnętrznej bo oczywiście nie jest to zwykłe zdarzenie rozproszone…

Planowanie zadań, nServiceBus–część VI

Dzisiaj o zadaniu, które jest powszechne dla systemów rozproszonych, zwłaszcza tych opartych o kolejki. Załóżmy, że chcemy wysyłać emaile co 24h.  Najpierw definiujemy wiadomość:

public class SendEmail : IMessage
{
   public string Email { get; set; }
}

Kolejnym krokiem jest implementacja handlera:

public class SendEmailHandler : IHandleMessages<SendEmail>
{
   public void Handle(SendEmail message)
   {
       Console.WriteLine("{0}:{1}", DateTime.Now.ToLongTimeString(), message.Email);
   }
}

Jeśli chcemy co 5 sekund umieszczać powyższą wiadomość na kolejce, to możemy:

_schedule.Every(TimeSpan.FromSeconds(10), () => _bus.SendLocal(new SendEmail { Email = Guid.NewGuid().ToString() }));

Zmienna _schedule to instancja klasy Schedule. Najlepszym momentem na wywołanie tego jest prawdopodobnie start aplikacji:

public class ScheduleMyTasks : IWantToRunWhenBusStartsAndStops
{
   private readonly IBus _bus;
   private readonly Schedule _schedule;

   public ScheduleMyTasks(IBus bus, Schedule schedule)
   {
       _bus = bus;
       _schedule = schedule;
   }

   public void Start()
   {
       _schedule.Every(TimeSpan.FromSeconds(10), () => _bus.SendLocal(new SendEmail()));
   }

   public void Stop()
   {
   }
}

Implementując interfejs IWantToRunWhenBusStartsAndStops możemy wykonać własny kod w momencie startu nServiceBus. Nic  więcej nie musimy konfigurować – klasa sama zostanie znaleziona przez framework.

Należy mieć na uwadze, że scheduler umieści wiadomość w kolejce co określony czas, ale nie jest to jednoznaczne z wykonaniem kodu. Jeśli sporo wiadomości czeka na handler’a to oczywiście może to opóźnić się.

Tak jak to w przypadku wszystkich wiadomości, gdy obsługa zakończy się wyjątkiem, wtedy zostanie ona powtórzona kilkakrotnie (pisałem w poprzednich postach o mechanizmie wznowień). Uruchamiając przykład zobaczymy zatem:

image

Wzorzec Saga, nServiceBus- część V

Dzisiaj o implementacji wzorca saga w nServiceBus. Korzystamy z niego, jeśli mamy kilka procesów biznesowych, które współtworzą jeden rozbudowany workflow. Załóżmy, że w celu złożenia zamówienia, należy wykonać kilka operacji takich jak weryfikacja danych, zapisanie danych w bazie, wysłanie emaila itp. Jeśli operacje są niezależne od siebie, wtedy wystarczy wysłać odpowiednie wiadomości do odizolowanych od siebie handlerów, tak jak to miało miejsce w poprzednich wpisach.

Co jeśli pewien stan jest współdzielony? Wtedy wspomniane podejście nie zadziałała. Rozwiązaniem jest wzorzec sagi. Można ją porównać do transakcji z tym, że nie ma tutaj koncepcji ACID. Jedynie co mamy to stan, który będzie współdzielony przez wszystkie etapy sagi. Z punktu widzenia nServiceBus, jest to klasa, która obsługuje kilka wiadomości naraz. Będziemy mieli zatem kilka implementacji metody Handle, dla różnych wiadomości. Oprócz tego, stan jest współdzielony między wszystkie handlery i zapisywany odpowiednio w bazie danych przez nServiceBus. W programowaniu rozproszonym nie wiemy, ile czasu ten stan będzie musiał być przechowywany w pamięci. Wywoływanie zewnętrznych serwisów może potrwać od sekundy po kilka godzin lub dni. Jak wspomniałem wcześniej, główna koncepcja nServiceBus i kolejek to  zagwarantowanie, że każda wiadomość zostanie dostarczona, nawet w przypadku tymczasowej awarii sieci. Z tego względu, niezbędne jest zapisanie stanu w bazie danych, a nie tylko do pamięci podręcznej.

Myślę, że przykład wyjaśni najlepiej powyższy wzorzec, który wciąż nie jest bardzo znany. Zacznijmy od deklaracji stanu, który będzie współdzielony przez wszystkie etapy sagi:

public class MySagaData : ContainSagaData
{
   [Unique]
   public Guid SagaId { get; set; }

   public string CustomField1 { get; set; }
   public string CustomField2 { get; set; }
   public string CustomField3 { get; set; }
}

W nServiceBus każdy stan musi dziedziczyć po ContainSagaData. Saga również powinna mieć identyfikator. W końcu chcemy współdzielić stan w ramach konkretnej sagi. Innymi słowy,  gdy rozpoczniemy dwie niezależne od siebie sagi, powinny one mieć osobne stany.

Samą sagę definiujemy, poprzez dziedziczenie po klasie Saga:

class SagaExample : Saga<MySagaData>
{
   protected override void ConfigureHowToFindSaga(SagaPropertyMapper<MySagaData> mapper)
   {
       throw new NotImplementedException();
   }
}

W ConfigureHowToFindSaga definiuje się korelacje między wiadomościami a sagą. Następnie musimy zdefiniować co rozpocznie daną sagę. W nServiceBus będzie to oczywiście wiadomość np.:

public class AddPerson:IMessage
{
   public Guid PersonId { get; set; }
}

Za pomocą interfejsu IAmStartedByMessages, informujemy, że dana wiadomość (AddPerson) zawsze rozpocznie nową sagę:

class SagaExample : Saga<MySagaData>, IAmStartedByMessages<AddPerson>
{
   protected override void ConfigureHowToFindSaga(SagaPropertyMapper<MySagaData> mapper)
   {
       throw new NotImplementedException();
   }

   public void Handle(AddPerson message)
   {
       throw new NotImplementedException();
   }
}

Kolejnym etapem jest zdefiniowanie kilku wiadomości, które będą współtworzyć sagę. W naszym przypadku:

public class FirstNameMessage : IMessage
{
   public Guid PersonId { get; set; }
   public string FirstName { get; set; }
}

public class LastNameMessage : IMessage
{
   public Guid PersonId { get; set; }
   public string LastName { get; set; }
}

Saga zatem aktualnie wygląda następująco:

class SagaExample : Saga<MySagaData>, IAmStartedByMessages<AddPerson>, IHandleMessages<FirstNameMessage>,IHandleMessages<LastNameMessage>
{
   protected override void ConfigureHowToFindSaga(SagaPropertyMapper<MySagaData> mapper)
   {
       throw new NotImplementedException();
   }

   public void Handle(AddPerson message)
   {
       throw new NotImplementedException();
   }

   public void Handle(FirstNameMessage message)
   {
       throw new NotImplementedException();
   }

   public void Handle(LastNameMessage message)
   {
       throw new NotImplementedException();
   }
}

Innymi słowy, saga składa się ze stanu, wiadomości rozpoczynającej proces (sagę) oraz wiadomości składających się na nią. Musimy teraz skonfigurować korelację między poszczególnymi wiadomościami a współdzielonym stanem. W naszym przypadku PersonID będzie określał sagę.

We wspomnianej metodzie ConfigureHowToFindSaga, definiujemy mapowanie:

protected override void ConfigureHowToFindSaga(SagaPropertyMapper<MySagaData> mapper)
{
  mapper.ConfigureMapping<FirstNameMessage>(s => s.PersonId).ToSaga(m => m.SagaId);
  mapper.ConfigureMapping<LastNameMessage>(s => s.PersonId).ToSaga(m => m.SagaId);
}

Następnie, w obsłudze wiadomości, która rozpoczyna sagę inicjalizujemy stan:

public void Handle(AddPerson message)
{
  Data.SagaId = message.PersonId;
  Data.CustomField1 = "any value";
  Data.CustomField2 = "any value 1";
  Data.CustomField3 = "any value 2";
}

Najważniejsze pole to SagaID. Reszta jest opcjonalna i może zostać wypełniona na każdym etapie sagi. Zaimplementujmy pozostałe wiadomości w następujący sposób:

class SagaExample : Saga<MySagaData>, IAmStartedByMessages<AddPerson>, IHandleMessages<FirstNameMessage>, IHandleMessages<LastNameMessage>
{
   protected override void ConfigureHowToFindSaga(SagaPropertyMapper<MySagaData> mapper)
   {
       mapper.ConfigureMapping<FirstNameMessage>(s => s.PersonId).ToSaga(m => m.SagaId);
       mapper.ConfigureMapping<LastNameMessage>(s => s.PersonId).ToSaga(m => m.SagaId);
   }

   public void Handle(AddPerson message)
   {
       Data.SagaId = message.PersonId;
       Data.CustomField1 = "any value";
       Data.CustomField2 = "any value 1";
       Data.CustomField3 = "any value 2";

       Console.WriteLine("Handling AddPerson");
       PrintState();
   }

   public void Handle(FirstNameMessage message)
   {
       Data.CustomField2 = message.FirstName;

       Console.WriteLine("Handling FirstNameMessage");
       PrintState();       
   }

   public void Handle(LastNameMessage message)
   {
       Data.CustomField3 = message.LastName;

       Console.WriteLine("Handling LastNameMessage");
       PrintState();
   }

   private void PrintState()
   {
       Console.WriteLine(Data.SagaId);
       Console.WriteLine(Data.CustomField1);
       Console.WriteLine(Data.CustomField2);
       Console.WriteLine(Data.CustomField3);
       Console.WriteLine();
   }
}

Spróbujmy przetestować naszą sagę wysyłając serie następujących wiadomości:

var busConfiguration = new BusConfiguration();
busConfiguration.UsePersistence<RavenDBPersistence>();
ISendOnlyBus bus = Bus.CreateSendOnly(busConfiguration);

Guid personId = Guid.NewGuid();
bus.Send(new AddPerson() {PersonId = personId});
bus.Send(new FirstNameMessage() { PersonId = personId,FirstName = "Piotr"});
bus.Send(new LastNameMessage() { PersonId = personId, LastName = "Zielinski" });

Prześledźmy co po kolei ma miejsce:

image

Najpierw wysyłamy AddPerson, co inicjuje nową sagę. Następnie odbierane są FirstNameMessage oraz LastNameMessage. Widzimy wyraźnie, że stan jest przechowywany pomiędzy kolejnymi wiadomościami.

Spróbujmy teraz wysłać FirstNameMessage oraz LastNameMessage z identyfikatorem sagi, która nie istnieje tzn.:

bus.Send(new AddPerson() {PersonId = Guid.NewGuid()});            
Thread.Sleep(5000);
bus.Send(new FirstNameMessage() { PersonId = Guid.NewGuid(),FirstName = "Piotr"});
Thread.Sleep(5000);
bus.Send(new LastNameMessage() { PersonId = Guid.NewGuid(), LastName = "Zielinski" });

Za każdym razem generujemy nowy identyfikator. Z tego względu tylko AddPerson zostanie obsłużone bo ta wiadomość rozpoczyna nową sagę. Następne wiadomości zakończą się błędem ponieważ posiadają ID nieistniejącej sagi:

image

Zróbmy jeszcze jeden eksperyment. Co się stanie jeśli wyślemy trzy razy tą samą wiadomość?

Guid personId = Guid.NewGuid();

bus.Send(new AddPerson() { PersonId = personId });
bus.Send(new FirstNameMessage() { PersonId = personId, FirstName = "Piotr1" });
bus.Send(new FirstNameMessage() { PersonId = personId, FirstName = "Piotr2" });
bus.Send(new FirstNameMessage() { PersonId = personId, FirstName = "Piotr3" });

Wynik:

image

Jak widzimy, wszystkie wiadomości zostały prawidłowo obsłużone. Wynika to z tego, że nie zamknęliśmy sagi. Można tego dokonać za pomocą metody MarkAsCompleted. Wysłanie kilkukrotne FirstNameMessage poskutkuje teraz:

image

Jak widzimy, tylko raz wiadomość zostanie obsłużona. Kolejne wywołania prowadzą do błędu, ponieważ po pierwszym odebraniu FirstNameMessage, saga jest zakończona. W moich postach korzystam z RavenDB i zaglądając do bazy, przekonamy się, że stan sagi faktycznie jest tam przechowywany:

image

Dla przypomnienia, bazę konfigurowaliśmy (patrz poprzednie wpisy) w EndPointConfiguration:

public class EndpointConfig : IConfigureThisEndpoint, AsA_Server
{
   public void Customize(BusConfiguration configuration)
   {
      configuration.UsePersistence<RavenDBPersistence>();
   }
}

Zaglądając do bazowej klasy ContainSagaData zobaczymy:

public abstract class ContainSagaData : IContainSagaData
  {
    /// <summary>
    /// The saga id
    /// 
    /// </summary>
    public virtual Guid Id { get; set; }
    /// <summary>
    /// The address io the endpoint that started the saga
    /// 
    /// </summary>
    public virtual string Originator { get; set; }
    /// <summary>
    /// The id of the message that started the saga
    /// 
    /// </summary>
    public virtual string OriginalMessageId { get; set; }
  }

Widzimy, że dzięki nServiceBus wiemy kto rozpoczął sagę za pomocą Originator oraz OriginalMessageId.  W naszym przypadku jest to klient, ale zwykle jest to jakiś endpoint. Z tego względu, możemy odpowiedzieć bezpośrednio mu za pomocą:

 ReplyToOriginator(new AnyMessage { FirstName = Data.CustomField2 });

Mamy również do dyspozycji timeout. Jeśli wiemy, że obsługa sagi nie ma sensu, gdy przetwarzanie zajęło dłużej niż określony czas, wtedy możemy zaimplementować interfejs IHandleTimeouts:

 class SagaExample : Saga<MySagaData>, IAmStartedByMessages<AddPerson>, 
        IHandleMessages<FirstNameMessage>, 
        IHandleMessages<LastNameMessage>,
        IHandleTimeouts<MyCustomTimeout>
    {
    // ...
    }

Kolejnym krokiem jest wywołanie RequestTimeout z określonym czasem:

public void Handle(AddPerson message)
{
  Data.SagaId = message.PersonId;
  RequestTimeout<MyCustomTimeout>(TimeSpan.FromHours(1));
}

W tym przypadku jest to 1h. Po tym czasie, zostanie wywołana metoda Timeout (część interfejsu IHandleTimeouts):

public void Timeout(MyCustomTimeout state)
{
    ReplyToOriginator(new ErrorMessage(){PersonId=Data.SagaId});
}

W powyższym przykładzie,  wywołuję ReplyToOrignator co ma sens, ponieważ chcemy poinformować handler, który rozpoczął sagę o timeout.

MyCustomTimeout to zwykła struktura danych. Zaglądając do sygnatur RequestTimeout przekonamy się, jak jest ona wypełniana:

protected void RequestTimeout<TTimeoutMessageType>(TimeSpan within, Action<TTimeoutMessageType> messageConstructor) where TTimeoutMessageType : new();

C# 6.0: Using i klasy statyczne oraz metody asynchroniczne w catch\finally

Dzisiaj znów kilka drobnych nowości z C# 6.0 Pierwsza z nich to możliwość połączenia using z klasami statycznymi, których sposób użycia przypomina trochę przestrzenie nazw. Zaprezentuję to na przykładzie klasy Console. Posiada ona kilka statycznych metod m.in. WriteLine:

Console.WriteLine("Hello World!");

W nowej wersji, będziemy mogli dołączyć każdą klasę statyczną, tak jak zwykłą przestrzeń nazw:

using System.Console;

namespace ConsoleApplication2
{  
    class Program
    {
        static void Main(string[] args)
        {
            WriteLine("Hello World!");
   
        }     
    }
}


Ze względu, że jest to zwykły using, możemy również:

using Helper=System.Console;

namespace ConsoleApplication2
{
    class Program
    {
        static void Main(string[] args)
        {
            Helper.WriteLine("Hello World!");

        }
    }
}

Może to być bardzo przydatne, gdy mamy jakieś konflikty wśród nazw, które czasami zdarzają się dla helperów. Kod zdekompilowany z ILSpy nie powinien budzić zaskoczenia:

// ConsoleApplication2.Program
private static void Main(string[] args)
{
    Console.WriteLine("Hello World!");
}

Kolejną, dobrą zmianą jest możliwość korzystania z async\await w catch oraz finally. Poniższy kod nie skompilowałby się w poprzednich wersjach języka:

private async void Test()
{
  try
  {

  }
  catch
  {
      int result1 = await AsyncMethod();
  }
  finally
  {
      int result2 = await AsyncMethod();
  }
}

private Task<int> AsyncMethod()
{
  return Task.FromResult(5);
}

Możliwe z kolei zawsze było wywoływanie asynchronicznych metod (await) w try – co byłoby ogromnym ograniczeniem, gdyby było zabronione.

Jak widać, nie są to rewolucyjne zmiany, ale warto o nich pamiętać.

Obsługa błędów w nServiceBus (część IV)

W dzisiejszym wpisie pokażę jak nServiceBus radzi sobie z błędami. W środowisku rozproszonych, opartym o kolejki, możliwe jest, że będziemy mieli do czynienia z tymczasowymi błędami. Może to być np. timeout związany z próbą połączenia się z usługą lub deadlock w bazie danych.  Z tego względu, nServiceBus posiada kilkuwarstwowy mechanizm wznawiania wiadomości.

Jeśli przetwarzanie zakończy się błędem, wiadomość nie jest od razu umieszczana w kolejce errors. Najpierw handler jest powtarzany przez określoną liczbę razy (domyślnie 5). Dzięki temu, tymczasowe błędy takie jak deadlock czy timeout mogą zostać zniwelowane. Oczywiście, czasami natychmiastowa ponowna próba jest zbyt szybka. Z tego względu, istnieje druga warstwa mechanizmu recovery. Jeśli pierwsze 5 prób zakończy się niepowodzeniem, wtedy nServiceBus będzie robił przerwy przed kolejnymi próbami i stanowi to drugą warstwę recovery.

Stwórzmy handler, który będzie wyrzucał wyjątek:

class AddPersonHandler:IHandleMessages<AddPerson>
{
   public void Handle(AddPerson message)
   {
       Console.WriteLine(DateTime.Now.ToLongTimeString());
       
       throw new Exception();
   }
}

Na początku, nServiceBus będzie próbował zastosować mechanizm z pierwszej warstwy co poskutkuje pięciokrotnym wykonaniem Handle, jeden po drugim. Jeśli chcemy zmienić wartość domyślną powtórzeń, możemy to w pliku konfiguracyjnym określić:

<MsmqTransportConfig MaxRetries="5" />

Następnie zostanie zastosowana druga warstwa, która ma na celu naprawienie błędów tymczasowych, które jednak mogą potrwać kilka sekund. Domyślne zachowanie można skonfigurować za pomocą:

<SecondLevelRetriesConfig Enabled="true" TimeIncrease="00:00:10" NumberOfRetries="3" />

Widzimy, że będziemy mieli trzy próby, w odstępach 10, 20 oraz 30 sekund (TimeIncrease). Innymi słowy, każda kolejna próba wiążę się z coraz dłuższą przerwą.

Jeśli błąd jest permanentny, wtedy naturalnie żaden mechanizm retry nie zadziała i wiadomość zostanie przekazana do kolejki błędów (errors). Można również skonfigurować, która kolejka będzie zawierała  wiadomości, które nie można było przetworzyć za pomocą:

<MessageForwardingInCaseOfFaultConfig ErrorQueue="error"/>

Możemy zajrzeć do Computer Management i przekonać się, że faktycznie nasza wiadomość została umieszczona w kolejce errors:

image

Należy uważać, na konfiguracje liczby ponowień, ponieważ zbyt wysoka liczba może spowodować spore problemy wydajnościowe. Nie chcemy w końcu w nieskończoność ponawiać wykonania jeśli wiemy, że błędy są permanentne. Metoda w końcu może zawierać skomplikowaną logikę i inne czasochłonne operacje. Każde ponowne wykonanie takiej metody, powoduje zużycie zasobów komputera.