nServiceBus, spojrzenie na kolejki w Windows (część III)

Wracamy do serii wpisów o nServiceBus. Koniecznie zapraszam najpierw do przeczytania następujących postów:

http://www.pzielinski.com/?cat=29

Szczególnie pierwszy wpis o teoretycznych zagadnieniach będzie tutaj ważny. Dzisiaj zajmiemy się na przykładzie jak wędruje wiadomość od jednego węzła do drugiego oraz co stanie się, gdy serwer nie będzie dostępny.

Dla przypomnienia, w poprzednim wpisie stworzyliśmy handler:

class AddPersonHandler:IHandleMessages<AddPerson>
{
   public void Handle(AddPerson message)
   {
       Console.WriteLine("Dodawanie osoby: {0} {1}",message.FirstName,message.LastName);
   }
}

Komendę AddPerson:

public class AddPerson:IMessage
{
   public string FirstName { get; set; }
   public string LastName { get; set; }
}

A także klienta, który wysyła wiadomości w następujący sposób:

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

while (true)
{
    string firstName = Console.ReadLine();
     string lastName = Console.ReadLine();

     bus.Send(new AddPerson() {FirstName = firstName, LastName = lastName});
}

Jak już wiemy z pierwszego wpisu, z kolejek korzystamy m.in. wtedy, gdy zależy nam na niezawodności dostarczenia wiadomości. System kolejkowy odporny jest na sytuacje, gdy połączenie sieciowe między węzłami jest przerwane tymczasowo.  nServiceBus (a raczej kolejki) zadbają, aby wiadomość została dostarczona, jak tylko połączenie zostanie przywrócone.

Zróbmy więc eksperyment. Odpalmy tylko klienta i wyślijmy kilka wiadomości:

image

Jak widać, wysłanie powiodło się. Nie widać tego na screen’ie, ale fakt, że serwer nie jest uruchomiony, nie przeszkadza w żaden sposób na wysłanie wiadomości za pomocą:

  bus.Send(new AddPerson() {FirstName = firstName, LastName = lastName});

Nie było również żadnego blokowania jak to w przypadku synchronicznych wywołań. Oczywiście, jak zostało to napisane w pierwszym poście, wiadomość została umieszczona w kolejce wiadomości wychodzących i potem przychodzących:

image

Zajrzyjmy zatem do kolejek. W tym celu odpalamy Computer Management i przechodzimy do Services and Applications –> Message Queuing:

image

Kolejka w której umieszczane są wiadomości do przetworzenia, to w tym przypadku Example.API (taką dałem nazwę projektowi, w którym znajdują się handler’y):

image

Są tam dokładnie dwie pozycje, ponieważ dane dwóch osób wpisaliśmy w konsoli. Klikając na jedną z nich, dowiemy się o szczegółach:

image

Widzimy, że reprezentuje to dokładnie naszą wiadomość AddPerson.

Po uruchomieniu serwera, wiadomości zostaną zdjęte z kolejki i przetworzone:

image

Klient zatem nie interesuje się kiedy serwer otrzyma to wiadomość, co jest przeciwieństwem RPC. Widzimy tam również trzy inne kolejki, którym przyjrzyjmy się kiedyś indziej, ale służą one do obsługi błędów.

Wiadomość może zostać odebrana dopiero np. po kilku dniach. Jeśli z punktu logiki biznesowej, wiemy, że nie ma sensu przetwarzać danej wiadomości po jakimś czasie, to można posłużyć się atrybutem TimeToBeReceived:

[TimeToBeReceived("00:00:10")]
public class AddPerson:IMessage
{
   public string FirstName { get; set; }
   public string LastName { get; set; }
}

Jeśli AddPerson nie zostanie dostarczony w ciągu 10 sekund, system usunie wiadomość z kolejki.

Na początku serii wpisów pisałem o kolejkach wiadomości przychodzących i wychodzących. W powyższym wpisie, pokazałem wyłącznie jedną kolejkę  Wynika to z faktu, że prezentuje to na jednym, tym samym komputerze. nServiceBus po prostu rozpozna to i kolejka wiadomości wychodzących klienta stanowi tak naprawdę kolejkę wiadomości przychodzących serwera. Jeśli korzystaliśmy z serwera zdalnego, wtedy byśmy mieli jeszcze jedną kolejkę  w sekcji Outgoing Queues.

C# 6.0: Definiowanie metod za pomocą wyrażenia lambda

Dzisiaj kolejna nowa funkcjonalność w c#. Zacznijmy od przykładu:

public class Point
{
   public double Dist => Math.Sqrt(X * X + Y * Y);
   public double X;
   public double Y;
}

X oraz Y to zwykłe pola (tak nie powinno się ich  definiować jako publiczne ale to tylko przykład). Następnie Dist to dziwny twór… Wiemy, że mamy tam wyrażenie lambda, które wywołuje Math.Sqrt i robi obliczenia. Zobaczymy jak możemy  z tego skorzystać w kodzie:

Point point = new Point();
point.X = 5;
point.Y = 6;

double dist = point.Dist;
Console.WriteLine(dist);

Dist to po prostu zwykła właściwość. Zaglądając do IL na zdekompilowany kod c# ujrzyjmy:

public class Point
{
    public double X;
    public double Y;
    public double Dist
    {
        get
        {
            return Math.Sqrt(this.X * this.X + this.Y * this.Y);
        }
    }
}

Dla tych co preferują IL:

.class public auto ansi beforefieldinit ConsoleApplication2.Point
    extends [mscorlib]System.Object
{
    // Fields
    .field public float64 X
    .field public float64 Y

    // Methods
    .method public hidebysig specialname 
        instance float64 get_Dist () cil managed 
    {
        // Method begins at RVA 0x2050
        // Code size 33 (0x21)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: ldfld float64 ConsoleApplication2.Point::X
        IL_0006: ldarg.0
        IL_0007: ldfld float64 ConsoleApplication2.Point::X
        IL_000c: mul
        IL_000d: ldarg.0
        IL_000e: ldfld float64 ConsoleApplication2.Point::Y
        IL_0013: ldarg.0
        IL_0014: ldfld float64 ConsoleApplication2.Point::Y
        IL_0019: mul
        IL_001a: add
        IL_001b: call float64 [mscorlib]System.Math::Sqrt(float64)
        IL_0020: ret
    } // end of method Point::get_Dist

    .method public hidebysig specialname rtspecialname 
        instance void .ctor () cil managed 
    {
        // Method begins at RVA 0x2072
        // Code size 7 (0x7)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: call instance void [mscorlib]System.Object::.ctor()
        IL_0006: ret
    } // end of method Point::.ctor

    // Properties
    .property instance float64 Dist()
    {
        .get instance float64 ConsoleApplication2.Point::get_Dist()
    }

} // end of class ConsoleApplication2.Point

Nie lubię tej składni ponieważ wyobraźmy sobie, że mamy:

public class Point
{
   public double GetDist => Math.Sqrt(X * X + Y * Y);
   public double X;
   public double Y;
}

Z punktu czytelnika ma to sens. GetDist i wyrażenie lambda. Kompilator jednak nie wygeneruje metody i poniższy kod zakończy się błędem kompilacji:

Point point = new Point();
point.X = 5;
point.Y = 6;

double dist = point.GetDist();
Console.WriteLine(dist);

Z kolei bardzo podoba konstrukcja poskutkuje wygenerowaniem metody:

public class Point
{
   public double GetDist()=> X*Y;
   public double X;
   public double Y;
}

class Program
{
   static void Main(string[] args)
   {
       Point point = new Point();
       point.X = 5;
       point.Y = 6;

       double dist = point.GetDist();
       Console.WriteLine(dist);

   }     
}

Zdekompilowany kod:

public class Point
{
    public double X;
    public double Y;
    public double GetDist()
    {
        return this.X * this.Y;
    }
}

Możemy również w podobny sposób dodawać parametry tzn.:

public class Point
{
   public double GetDist(int z)=> X*Y*z;
   public double X;
   public double Y;
}

class Program
{
   static void Main(string[] args)
   {
       Point point = new Point();
       point.X = 5;
       point.Y = 6;

       double dist = point.GetDist(3);
       Console.WriteLine(dist);
   }     
}

To już wygeneruje normalną metodę:

public class Point
{
    public double X;
    public double Y;
    public double GetDist(int z)
    {
        return this.X * this.Y * (double)z;
    }
}

Rozwiązane fajne, ale należy stosować z rozwagą. Szczególnie nie podoba mi się, że nie jest to jasne kiedy właściwość, a kiedy metoda zostanie wygenerowana.  Moim zdaniem nie wynika to wystarczająco z samej składni.

C# 6.0–bezparametrowe konstruktory oraz inicjalizacja automatycznych właściwości w konstruktorze

W nowej wersji c# zdecydowano się zezwolić na konstruktory bezparametrowe w strukturach danych. Kiedyś pisałem dlaczego, nie można było ich definiować w poprzednich wersjach języka. Chodziło po prostu o wydajność, szczególnie podczas alokacji tablic danych. Dodam, że w CLR zawsze było dozwolone posiadanie konstruktorów bezparametrowych i wyłącznie c# na to nie zezwalał.

W C# 6.0 możliwe już jest napisanie własnego konstruktora bez parametrów, ale wciąż należy pamiętać o możliwych problemach wydajnościowych. Zobaczmy na dwie analogiczne struktury:

public struct SampleStructWithDefaultCtor
{
   public SampleStructWithDefaultCtor()
   {
   }

   private int _x=0, _y=0;
}

public struct SampleStructWithoutDefaultCtor
{
   private int _x, _y;
}

Jedna z nich pomimo, że jest dozwolona w C# 6.0, wciąż będzie dużo wolniejsza. Zróbmy test:

class Program
{
   static void Main(string[] args)
   {
       const int n = 5000000;

       Stopwatch stopwatch = Stopwatch.StartNew();
       var sampleStructWithDefaultCtor = new SampleStructWithDefaultCtor[n];
       Console.WriteLine(stopwatch.ElapsedTicks);

       stopwatch = Stopwatch.StartNew();
       var sampleStructWithoutDefaultCtor = new SampleStructWithoutDefaultCtor[n];
       Console.WriteLine(stopwatch.ElapsedTicks);
   }     
}

Wynik:

image

Oczywiście różnica jest znacząca jednak w wielu przypadku jest to zwyczajnie mikro-optymalizacja.  Myślę, że po prostu zdecydowano się to ponieważ strata w wydajności nie ma aż tak wielkiego znaczenia, a konstrukcja jest czasami prosta. Z drugiej jednak strony, korzystamy ze struktur kiedy operujemy na bardzo dużej ilości danych i wtedy już taka różnica w wydajności może mieć znaczenie. Po prostu trzeba być świadomym implementacji wewnętrznej struktur i podejmować decyzję o architekturze w zależności od konkretnego przypadku.

Druga drobna rzecz o której mi umknęło, gdy pisałem o udogodnieniach w automatycznych właściwościach to ich inicjalizacja w konstruktorach. Wspomniałem, że istnieje możliwość deklaracji czystych getterów ponieważ wprowadzono inicjalizacje inline:

class Person
{
  public string FirstName { get; } ="Piotr";
  public string LastName { get; } = "Zielinski";
}  

Możliwe jest również inicjalizowanie tych wartości w konstruktorze:

class Person
{
   public Person()
   {
       FirstName = "Piotr";
       LastName = "Zielinski";
   }

   public string FirstName { get; }
   public string LastName { get; } 
}

Niestety w aktualnym CTP nie będziemy w stanie tego przetestować, ale najprawdopodobniej pojawi się to w oficjalnym wydaniu.

C# 6.0: operator nameof oraz propagacja NULL

Zacznijmy dziś od operatora nameof. Zwraca on po prostu nazwę przekazanej metody:

static void Main(string[] args)
{
  string writeLine= nameof(Console.WriteLine);
  string testFunction = nameof(TestFunction);

  Console.WriteLine(writeLine);
  Console.WriteLine(testFunction);

}
private static string TestFunction(int a,double b)
{
  return null;
}

Chyba nie ma tutaj nic nadzwyczajnego. Po prostu zwracana jest nazwa w formie string:

image

Po co nam to? Na przykład, aby zaimplementować OnPropertyChanged:

public class Person
{
   private string _firstName;

   public string FirstName
   {
       get { return _firstName; }
       set
       {
           _firstName = value;
           OnPropertyChanged(nameof(FirstName));
       }
   }

   private void OnPropertyChanged(string propertyName)
   {
       // ...
   }
}

Zawsze to lepsze niż atrybuty dodane w .NET 4.5. Generalnie dzięki temu, możemy uniknąć stringow które mogą powodować problemy potem podczas refaktoryzacji.

Odpalmy jeszcze ILSpy i przyjrzyjmy się jak kod wygląda po dekompilacji:

public string FirstName
{
    get
    {
        return this._firstName;
    }
    set
    {
        this._firstName = value;
        this.OnPropertyChanged("FirstName");
    }
}

Jak widać, po prostu pierwsza kompilacja (z C# do IL) zastąpiła nameof konkretną nazwą.

Kolejny według mnie bardzo ciekawy operator to tzw. null propagation operator. Załóżmy,  że mamy klasę:

class Person
{
   public Address Address { get; set; }
}

class Address
{
   public string TownName { get; set; }
}

W celu wyświetlenia adresu możemy:

Person person = new Person();
Console.WriteLine(person.Address.TownName);

Co jednak gdy address jest NULL? Dostaniemy wyjątek w brzydkim miejscu. Zwykle zaleca się korzystanie z prawa demeter. Czasami jednak jest to dość niewygodne bo kod się trochę komplikuje:

Person person = new Person();
Address address = person.Address;
Console.WriteLine(address.TownName);

Za pomocą nowego operatora możemy jednak:

Console.WriteLine(person.Address?.TownName ?? "Brak adresu");

Oczywiście ?. nie sprowadza się wyłącznie tylko do  jednej właściwości. Nic nie stoi na przeszkodzie abyśmy napisali:

Person person = null;
Console.WriteLine(person?.Address?.TownName ?? "NULL");

Operator zostanie zamieniony na zwykłe ify:

// ConsoleApplication3.Program
private static void Main(string[] args)
{
    Person person = null;
    string arg_1B_0;
    if (person == null)
    {
        arg_1B_0 = null;
    }
    else
    {
        Address expr_0F = person.Address;
        arg_1B_0 = ((expr_0F != null) ? expr_0F.TownName : null);
    }
    Console.WriteLine(arg_1B_0 ?? "NULL");
}

Jest to kolejne ułatwienie, z którego należy korzystać z umiarkowaniem. Jeśli są to tylko czyste właściwości, wtedy myślę, że można z tego skorzystać. Jeśli wywołujemy jakieś skomplikowane metody, wtedy lepiej rozdzielić kod na kilka linii bo jest to łatwiejsze w debuggowaniu.

Obsługa wyjątków w c# 6.0

Nowa wersja języka wprowadzi również ulepszenia w obsłudze wyjątków. Często tworzymy jeden wyjątek typu OperationFailedException, a w nim enum, który określa dlaczego operacja nie powiodła się tzn.:

public class OperationFailedException : Exception
{
   public OperationFailedException(int statusCode) { StatusCode = statusCode; }        

   public int StatusCode { get; set; }
}

Co jeśli chcemy napisać obsługę wyjątków, ale wyłącznie tych ze statusem 5? Dzisiaj możemy łapać wszystkie wyjątki i sprawdzać jaki status został zwrócony:

try
{
 throw new OperationFailedException(5);
}
catch(OperationFailedException e)
{
 if (e.StatusCode != 5)
     throw;

 // recovery
 Console.WriteLine("Recovery");
}

Czasami jest więcej warunków i sprawa po prostu komplikuje się. W C# 6.0 możliwe jest łapanie wyjątków za pomocą filtra tzn.:

 try
  {
      throw new OperationFailedException(5);
  }
  catch(OperationFailedException e) if(e.StatusCode==5)
  {       
      // recovery
      Console.WriteLine("Recovery");
  }

Wyłącznie OperationFailedException z StatusCode równym 5 będą wyłapywane. Oczywiście jak to ze wszystkimi ułatwieniami bywa, nie znaczy to, że powinniśmy tworzyć jeden wyjątek z wieloma statusami. Wciąż lepiej rozdzielać wyjątki na wiele klas. Czasami po prostu nie ma sensu tworzyć dwóch bardzo podobnych klas i lepiej zaimplementować to za pomocą jednej ale właśnie ze statusem.

Zajrzyjmy teraz do ILSpy:

private static void Main(string[] args)
{
    try
    {
        throw new OperationFailedException(5);
    }
    object arg_09_0;
    OperationFailedException expr_0E = arg_09_0 as OperationFailedException;
    int arg_22_0;
    if (expr_0E == null)
    {
        arg_22_0 = 0;
    }
    else
    {
        OperationFailedException e = expr_0E;
        arg_22_0 = (((e.StatusCode == 5) > false) ? 1 : 0);
    }
    endfilter(arg_22_0);
}

Tutaj widzimy pewną zmianę. Możemy zauważyć instrukcję endfilter. Tak naprawdę istniała ona od dawna w IL tylko C# nigdy jej nie wykorzystywał. Więcej informacji o instrukcji tutaj. W skrócie – to ona właśnie filtruje wyjątki na podstawie przekazanej flagi.

c# 6.0–inicjalizacja słowników oraz automatycznych właściwości

Najpierw zobaczymy, jak mogła wyglądać inicjalizacja słownika przed wersją 6.0:

var data = new Dictionary<string, int>()
{
 {"Klucz1",1 },
 {"Klucz2",114 },
 {"Klucz3",114 },
 {"Klucz5",41 }
};

Składnia trochę ciężka w czytaniu. W C# 6.0 możliwe będzie:

var data = new Dictionary<string, int>()
{
 ["Klucz1"] = 1,
 ["Klucz2"] = 114,
 ["Klucz3"] = 114,
 ["Klucz5"] = 41
};

Moim zdaniem dużo lepiej to wygląda. Drobna zmiana, a zwiększa czytelność kodu.

Kolejna natomiast zmiana nie podoba mi się i na szczęście została wycofana. Chciano wprowadzić nowy sposób uzyskiwania dostępu do pól. Klasycznie można odczytać wartość za pomocą:

var data = new Dictionary<string, int>()
{
    ["Klucz1"] = 1,
    ["Klucz2"] = 114,
    ["Klucz3"] = 114,
    ["Klucz5"] = 41
};

Console.WriteLine(data["Klucz1"]);

Alternatywą miało być:

var data = new Dictionary<string, int>()
  {
      $Klucz1 = 1,
      $Klucz2 = 114,
      $Klucz3 = 114,
      $Klucz4 = 41
  };

  Console.WriteLine(data.$Klucz1);

Z tego co wiem jednak, na szczęście nie będzie tego w c# 6.0

Kolejna nowość to inicjalizowanie automatycznych pól. Zawsze irytowało mnie, że samemu musiałem w konstruktorze przypisywać wartości tym właściwościom. Teraz możemy:

class Person
{
  public string FirstName { get; set; }="Piotr";
  public string LastName { get; set; }="Zielinski";
}    

Co prawda sama składnia jest trochę dziwna, ale do zaakceptowania. Nie wiem w sumie jakby to lepiej byłoby zaprojektować (może zwykły atrybut?). W każdym razie nie odstrasza mnie to tak bardzo jak planowany znak $ dla słowników.

Z powyższego wynika również, że można tworzyć właściwości tylko z getterem:

class Person
{
  public string FirstName { get; } ="Piotr";
  public string LastName { get; } = "Zielinski";
}    

Zaglądając do IL, przekonamy się, że nie jest to żadna zmiana w CLR tylko w czystym c# (co nie powinno dziwić):

.class nested private auto ansi beforefieldinit Person
    extends [mscorlib]System.Object
{
    // Fields
    .field private initonly string '<FirstName>k__BackingField'
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
        01 00 00 00
    )
    .custom instance void [mscorlib]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [mscorlib]System.Diagnostics.DebuggerBrowsableState) = (
        01 00 00 00 00 00 00 00
    )
    .field private initonly string '<LastName>k__BackingField'
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
        01 00 00 00
    )
    .custom instance void [mscorlib]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [mscorlib]System.Diagnostics.DebuggerBrowsableState) = (
        01 00 00 00 00 00 00 00
    )

    // Methods
    .method public hidebysig specialname 
        instance string get_FirstName () cil managed 
    {
        .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
            01 00 00 00
        )
        // Method begins at RVA 0x206c
        // Code size 11 (0xb)
        .maxstack 1
        .locals init (
            [0] string
        )

        IL_0000: ldarg.0
        IL_0001: ldfld string ConsoleApplication2.Program/Person::'<FirstName>k__BackingField'
        IL_0006: stloc.0
        IL_0007: br.s IL_0009

        IL_0009: ldloc.0
        IL_000a: ret
    } // end of method Person::get_FirstName

    .method public hidebysig specialname 
        instance string get_LastName () cil managed 
    {
        .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
            01 00 00 00
        )
        // Method begins at RVA 0x2084
        // Code size 11 (0xb)
        .maxstack 1
        .locals init (
            [0] string
        )

        IL_0000: ldarg.0
        IL_0001: ldfld string ConsoleApplication2.Program/Person::'<LastName>k__BackingField'
        IL_0006: stloc.0
        IL_0007: br.s IL_0009

        IL_0009: ldloc.0
        IL_000a: ret
    } // end of method Person::get_LastName

    .method public hidebysig specialname rtspecialname 
        instance void .ctor () cil managed 
    {
        // Method begins at RVA 0x209b
        // Code size 30 (0x1e)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: ldstr "Piotr"
        IL_0006: stfld string ConsoleApplication2.Program/Person::'<FirstName>k__BackingField'
        IL_000b: ldarg.0
        IL_000c: ldstr "Zielinski"
        IL_0011: stfld string ConsoleApplication2.Program/Person::'<LastName>k__BackingField'
        IL_0016: ldarg.0
        IL_0017: call instance void [mscorlib]System.Object::.ctor()
        IL_001c: nop
        IL_001d: ret
    } // end of method Person::.ctor

    // Properties
    .property instance string FirstName()
    {
        .get instance string ConsoleApplication2.Program/Person::get_FirstName()
    }
    .property instance string LastName()
    {
        .get instance string ConsoleApplication2.Program/Person::get_LastName()
    }

} // end of class Person

C# 6.0–primary constructors

Update [16.10.2014]: Niestety opisana konstrukcja prawdopodobnie nie znajdzie się w finalnej wersji C# 6.0. Możliwe, że wcześniej czy później zostanie to zaimplementowane, ale wiadomo, różnie z tym bywa.

Czas zacząć pisać o nowym c#.  Najpierw pobierzmy wersję CTP z:

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

Oczywiście to preview, więc odradzam instalowanie tego w pracy, chyba, że na VM. Lepiej nie ryzykować re-instalacją wszystkiego od nowa.

Często w kodzie można spotkać następujący wzorzec:

class Person
{
  private readonly string _firstName;
  private readonly string _lastName;

  public Person(string firstName,string lastName)
  {
      _firstName = firstName;
      _lastName = lastName;
  }
}

Kod często praktykowany, ale trochę nadmiarowy. Musimy zdefiniować konstruktor, tylko po to aby zainicjalizować kilka zmiennych. W C# do dyspozycji będziemy mieli następującą konstrukcję:

class Person(string firstName,string lastName)
{
  private readonly string _firstName=firstName;
  private readonly string _lastName = lastName;

  public override string ToString()
  {
      return string.Format("{0} {1}", _firstName, _lastName);
  }
}

Konstrukcja przydatna, aczkolwiek trochę dziwna. Nic takiego nie mieliśmy w CPP czy w Javie. Myślę jednak, że można przyzwyczaić się i jest to po prostu praktyczne.

Jest to najzwyklejszy konstruktor i Person zainicjalizujemy następująco:

Person person = new Person("piotr","zielinski");
Console.WriteLine(person.ToString());

Jeśli jednak będziemy chcieli skompilować to, dostaniemy błąd:

Error    1    Feature 'primary constructor' is only available in 'experimental' language version.    c:\ConsoleApplication2\ConsoleApplication2\Program.cs    17    21    ConsoleApplication2

Nic dziwnego. To tylko preview, nie ma pewności nawet czy ta konstrukcja będzie w oficjalnej wersji. Jeśli jednak chcemy to odpalić, przejdźmy do edycji pliku projektu i dodajmy tag LangVersion z ustawioną wartością “Experimental”:

  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <PlatformTarget>AnyCPU</PlatformTarget>
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>bin\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
    <LangVersion>Experimental</LangVersion>
  </PropertyGroup>

Próba użycia konstruktora bezparametrowego zakończy się również błędem:

Person person = new Person();

Tak jak wcześniej, zdefiniowanie niestandardowego konstruktora powoduje, że domyślny nie jest już automatycznie generowany.

Nic nie stoi na przeszkodzie, abyśmy pomieszali konstruktory:

class Person(string firstName,string lastName)
{
  private readonly string _firstName=firstName;
  private readonly string _lastName = lastName;
  private int _age;

  public Person(string firstName,string lastName,int age) :this(firstName,lastName)
  {
      _age = age;
  }
}

Oczywiście możemy  zdefiniować wyłącznie jeden primary constructor.

Zajrzyjmy również do IL:

.class nested private auto ansi beforefieldinit Person
    extends [mscorlib]System.Object
{
    // Fields
    .field private initonly string _firstName
    .field private initonly string _lastName
    .field private int32 _age

    // Methods
    .method public hidebysig specialname rtspecialname 
        instance void .ctor (
            string firstName,
            string lastName
        ) cil managed 
    {
        // Method begins at RVA 0x2067
        // Code size 22 (0x16)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: ldarg.1
        IL_0002: stfld string ConsoleApplication2.Program/Person::_firstName
        IL_0007: ldarg.0
        IL_0008: ldarg.2
        IL_0009: stfld string ConsoleApplication2.Program/Person::_lastName
        IL_000e: ldarg.0
        IL_000f: call instance void [mscorlib]System.Object::.ctor()
        IL_0014: nop
        IL_0015: ret
    } // end of method Person::.ctor

    .method public hidebysig specialname rtspecialname 
        instance void .ctor (
            string firstName,
            string lastName,
            int32 age
        ) cil managed 
    {
        // Method begins at RVA 0x207e
        // Code size 18 (0x12)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: ldarg.1
        IL_0002: ldarg.2
        IL_0003: call instance void ConsoleApplication2.Program/Person::.ctor(string, string)
        IL_0008: nop
        IL_0009: nop
        IL_000a: ldarg.0
        IL_000b: ldarg.3
        IL_000c: stfld int32 ConsoleApplication2.Program/Person::_age
        IL_0011: ret
    } // end of method Person::.ctor

} // end of class Person

Tutaj nic nie zmieniło się. Konstruktor został po prostu wygenerowany w klasyczny sposób.

nServiceBus, część II–pierwszy przykład obsługi wiadomości

W poprzednim w poście było dość teoretycznie, ale o to mi chodziło. Nie chciałem tworzyć wszystkich wpisów wyłącznie o API. Dzisiaj z kolei będzie już prosty przykład, na którym później będziemy testować to co zostało opisane w pierwszym poście.

Zacznijmy od zdefiniowania wiadomości, którą chcemy wysyłać. Zwykle tworzy się osobny projekt dla nich. Stwórzmy więc projekt Contracts i zainstalujmy nServicebBus:

image

Nasza wiadomość będzie nazywać się AddPerson:

public class AddPerson:IMessage
{
   public string FirstName { get; set; }
   public string LastName { get; set; }
}

Proszę zauważyć, że implementujemy IMessage. W praktyce korzysta się jeszcze z ICommand oraz IEvent, ale o tym kiedy indziej – w tym poście chce pokazać najprostszy przykład.

Powyższą klasę dodaliśmy do osobnego projektu Contracts, który będzie współdzielony zarówno przez serwer jak i klient.

Następnie możemy dodać projekt dla backend oraz zainstalować nServiceBus.Host:

image

Pakiet stworzy automatycznie klasę do konfiguracji hosta:

using NServiceBus;

/*
    This class configures this endpoint as a Server. More information about how to configure the NServiceBus host
    can be found here: http://particular.net/articles/the-nservicebus-host
*/
public class EndpointConfig : IConfigureThisEndpoint, AsA_Server
{
   public void Customize(BusConfiguration configuration)
   {
       configuration.UsePersistence<PLEASE_SELECT_ONE>();
   }
}

Jak widać musimy wybrać persistance model. Dla tego wpisu nie ma to większego znaczenia, ale będziemy posługiwać się bazą RavenDB. Oznacza to, że musimy zainstalować kolejny pakiet:

image

Konfiguracja zatem powinna wyglądać tak:

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

Oczywiście sami musimy zainstalować sobie bazę RavenDB. Na razie proszę nie martwić się o to, co będzie tam przechowywane (o tym innym razem).

Jeśli będzie nam brakować jakiś komponentów warto zainstalować skrypty PowerShell (z konsoli NuGet):

PM> Install-Package NServiceBus.PowerShell
PM> Import-Module .\packages\NServiceBus.PowerShell.4.3.0\lib\net40\NServiceBus.PowerShell.dll

Na przykład, aby skonfigurować performance counter, wystarczy:

PM> Install-NServiceBusPerformanceCounters

Pozostało nam dodać handler, który obsłuży wiadomość AddPerson:

class AddPersonHandler:IHandleMessages<AddPerson>
{
   public void Handle(AddPerson message)
   {
       Console.WriteLine("Dodawanie osoby: {0} {1}",message.FirstName,message.LastName);
   }
}

Kolejnym projektem będzie klient. Dla uproszczenia może być to aplikacja konsolowa.

Najpierw musimy stworzyć szynę danych po której będziemy wysyłać komendy i zdarzenia:

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

Wysłanie wiadomości wygląda następująco:

while (true)
{
 string firstName = Console.ReadLine();
 string lastName = Console.ReadLine();

 bus.Send(new AddPerson() {FirstName = firstName, LastName = lastName});
}

Powyższa próba wysłania wiadomości zostanie zakończona wyjątkiem:

Additional information: No destination could be found for message type Example.Contracts.AddPerson. Check the <MessageEndpointMappings> section of the configuration of this endpoint for an entry either for this specific message type or for its assembly.

Po prostu musimy zdefiniować, gdzie chcemy wysłać wiadomość. Domyślnie nazwa serwera to assembly name. Jeśli zatem, stworzyliśmy handler i serwer w Example.Backend to wystarczy:

bus.Send("Example.Backend@localhost",new AddPerson() {FirstName = firstName, LastName = lastName});

Oczywiście jest to mało eleganckie podejście. Lepiej jakbyśmy użyli pliku konfiguracyjnego. NuGet wygenerował już nam trochę wskazówek w config:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="UnicastBusConfig" type="NServiceBus.Config.UnicastBusConfig, NServiceBus.Core" />
  </configSections>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
    </startup>

  <UnicastBusConfig>
    <MessageEndpointMappings>
      <add Assembly="Example.Contracts" Endpoint="Example.Backend@localhost" />            
    </MessageEndpointMappings>
  </UnicastBusConfig>
</configuration>

Plik konfiguracyjny daje nam dużą swobodę. Możemy określić cały assembly, namespace, albo wyłącznie konkretny typ. Jak widać stanowi to po prostu mapowanie między typem wiadomości a endpoint’em. W przykładzie zaprezentowanym wszystkie wiadomości zdefiniowane w Example.Contracts będą wysyłane do Example.Backend.

Po uruchomieniu przykładu i wpisaniu imienia oraz nazwiska, wiadomość zostanie wysłana do serwera z uwzględnieniem systemu kolejek, o których pisałem parę dni temu.

W kolejnym wpisie przejrzymy dokładnie się, jak wiadomość wędruje między klientem a serwerem oraz co stanie się, gdy w międzyczasie padnie serwer.

Systemy oparte na kolejkach komunikatów na przykładzie nServiceBus, część I

W kilku postach mam zamiar opisać bibliotekę nServiceBus. Nie chodzi mi jednak o opis samego API, a zastanowienie się, kiedy warto z takiej architektury skorzystać. Większość programistów wciąż projektuje systemy na zasadzie klient-serwer. W wielu przypadkach jest to wystarczające rozwiązanie. Nie zawsze musimy tworzyć skalowalne rozwiązania. NoSQL, hadoop, chmura mają zastosowanie ale w wielu przypadkach jest to po prostu niepotrzebne. Nie każdy tworzy oprogramowanie, wykorzystywane przez miliony użytkowników. Wracając jednak do tematu. W przypadku nServiceBus chciałbym skupić się bardziej na scenariuszach w których klasyczne przetwarzanie zapytań może po prostu doprowadzić do zawieszenia systemu.

Klasyczny model to RPC – remote procedure call. Polega on na wywołaniu metody (web service, kontroler w MVC) i zwróceniu natychmiast wyniku. Jest to bardzo proste i  analogiczne do wywoływania zwykłej metody in-memory. Kod można pisać sekwencyjnie czyli natychmiast po wywołaniu metody będziemy posiadać dostęp do tego co wróci – bez potrzebny implementacji callback’ow. Ta zaleta (łatwość obsługi) bezpośrednio stanowi największą wadę, a mianowicie brak skalowalności. Zastanówmy się co może przytrafić nam się w momencie wysłania zapytania przez klienta:

  1. Serwer tymczasowo będzie niedostępny.
  2. Sieć zostanie odłączona.
  3. Sieć będzie bardzo obciążona, stąd na rezultat przyjdzie czekać dłużej niż zwykle.

W przypadku RPC, gdy serwer nie działa, oczywiście zapytanie zakończy się błędem lub blokowaniem klienta. Co jeśli kilka tysięcy klientów wyśle zapytanie w tej samej chwili? Prymitywna implementacja RPC stworzy kilka tysięcy wątków, co oczywiście będzie miało katastrofalne skutki. RPC przede wszystkim nie jest odporne na awarie zarówno po stronie klienta jak i serwera. Możliwe scenariusze to:

  1. Klient wysyła zapytanie ale serwer w międzyczasie ulega awarii stąd nie jest w stanie obsłużyć zapytania.
  2. Klient wysyła zapytanie, serwer obsługuje je ale w miedzy czasie klient ulega awarii. Z tego względu nie będzie możliwe otrzymanie odpowiedzi.

Do tego dochodzi czas wykonania powyższych operacji. RPC przyjmuje, że jest on stały i nie powinien blokować użytkownika na zbyt duży czas.

Kolejny problem to skalowalność. RPC to prosta architektura klient-serwer. Nie ma możliwości dystrybucji zapytań między różne węzły. Oczywiście to uproszczenie bo możemy mieć w końcu load balancer i kilka serwerów obsługujących zapytania. Im bardziej jednak będziemy zapytania “rozpraszać”, tym szybciej dojdziemy do architektury odmiennej od RPC.

Klasyczny model RPC możemy przedstawić zatem następująco:

image

Prosta sprawa, wysyłamy zapytanie i dostajemy od razu odpowiedź. Jeśli wywołamy kolejno metody A i B to najpierw uzyskamy wynik A a potem B – kolejność zachowana.

Przejdźmy teraz do wyjaśnienia jak działa nServiceBus i podobne technologie. Przede wszystkim mamy do dyspozycji kolejki. Serwer zamiast próbować obsłużyć wszystkie zadania jednocześnie, przechowuje dane na kolejce. W przypadku nServiceBus jest to MSMQ (Microsoft’owa implementacja). MSMQ to jest temat sam w sobie na długi artykuł albo książkę więc nie będę tutaj wyjaśniał API. Każdy węzeł (klient, serwer) posiada dwie grupy kolejek:

  1. kolejki wiadomości wychodzących
  2. kolejki wiadomości przychodzących

Dobrym porównaniem systemów opartych o komunikaty (wiadomości) jest poczta email. Jeśli osoba A wysyła email do  osoby B, to odbierze ona go, gdy będzie miała czas. Osoba A nie spodziewa się natychmiastowej odpowiedzi. Wysyła po prostu swoje pytanie (wiadomość). Następnie osoba B w wolnym czasie odbiera wiadomość, czyta i odpowiada. Analogicznie osoba A odczyta odpowiedź, gdy będzie miała czas.

Powróćmy znów do nServiceBus. Jeśli klient wysyła wiadomość do serwera będzie wyglądać to następująco:

image

Najpierw klient umieszcza wiadomość na wewnętrznej kolejce wiadomości wychodzących. Nawet jak połączenie między klientem a serwerem zostanie zerwane, wiadomość będzie przechowywana w kolejce i gotowa do wysłania, gdy tylko połączenie znów będzie działać. Następnie, po wysłaniu, zostanie ona umieszczona w kolejce wiadomości przychodzących drugiego węzła. Jeśli serwer jest bardzo zajęty, zostanie ona po prostu tam przechowywana aż do momentu, kiedy może zostać obsłużona. W pewnym momencie serwer zdejmie wiadomość, wykona dane zadanie i wygeneruje odpowiedź. Odpowiedz zwracana jest również w analogiczny sposób:

image

Serwer po wygenerowaniu odpowiedzi umieszcza ją w kolejce wiadomości wychodzących. Następnie, jak tylko będzie taka możliwość, jest ona przesyłana do klienta i umieszczana w kolejce komunikatów przychodzących. Klient potem zdejmuje ją z kolejki i obsługuje odpowiedź w odpowiedni sposób.

Dzięki dwóm typom kolejek, jesteśmy odporni na awarie sieci. Zmienia natomiast to sposób jak komunikujemy się z serwerem. Nie jest to model zapytanie-odpowiedź! Należy mieć to na uwadze, że nie ma gwarancji kiedy dostaniemy odpowiedź. Nie zawsze taki mechanizm nadaje się w danej aplikacji. Pojęcia klient oraz serwer również nie są prawidłowe i będę dalej posługiwać się słowem “węzeł”. Bardzo często w systematach opartych o kolejki mamy np. 5 węzłów i każdy z nich może przetwarzać te same zapytanie.

To dopiero początek. W przyszłym wpisie pokażę jakiś prosty system oparty o nServiceBus.  Więcej czasu w kolejnych wpisach również chcę przeznaczyć, jak nServiceBus dba o to, aby wiadomość dotarła i została przetworzona. Póki, co wiemy, że w przypadku awarii sieci, wiadomość pozostaje w kolejce. Co jednak w przypadku, gdy obsługa wiadomości zakończyła się wyjątkiem?