ASP.NET WebApi – testowanie routingu, część II

W poprzednim wpisie zajęliśmy się testowaniem routingu w WebAPI. Napisany kod był dość brzydki i warto go po prostu umieścić  w osobnej klasie,  tak abyśmy mogli z niego korzystać w różnych testach.

Zaznaczam, że wciąż nie jest to kod produkcyjny. Zdecydowanie nie będzie pokrywał wszystkich scenariuszy, dlatego odradzam umieszczenie go w swoim wewnętrznym repozytorium NuGet. Z drugiej jednak strony, nic nie szkodzi na przeszkodzie, aby korzystać z niego konkretnym projekcie i w razie potrzeby naprawić jego usterki.

Na GitHub znajduje się już inna biblioteka WebAPIContrib,  gdzie można skorzystać z bardzo dobrej implementacji:

https://github.com/WebApiContrib/WebAPIContrib/blob/master/src/WebApiContrib.Testing/RouteTestingExtensions.cs

Niestety ma ona referencje do zbyt wielu bibliotek, dlatego zdecydowałem napisać się własny mini-helper, który jest wzorowany na powyższym kodzie. Jeśli ktoś z Was chce użyć kodu w produkcji, zdecydowanie polecam przyjrzenie się WebApiContrig i potraktowanie mojego wpisu bardziej jako przewodnika a nie implementacji z której można korzystać (poniższy kod powstał w 10 minut…).

Tak jak w poprzednim poście, załóżmy, że mamy następujący kontroler:

public class SampleController : ApiController { [Route("site/{siteId}/person/{personid}/profile/words/{searchText}")] public int[] GetData(int siteId, int personId, string searchText) { return Enumerable.Range(1, 10).ToArray(); } }

Zamiast kopiować kod  z poprzedniego wpisu za każdym razem jak chcemy przetestować routing, chcemy użyć po prostu wyrażenia lambda np.:

const string url = "http://www.test.com/site/1/person/3/profile/words/ppp"; RoutingAssert.Verify<SampleController>(url, x => x.GetData(1, 3, "ppp"));

Pierwszy parametr to oczywiście URL a drugi to wyrażenie z którego możemy odczytać następujące informacje:

  1. Typ kontrolera
  2. Nazwę akcji
  3. Typ i wartości przekazanych parametrów.

Nie trudno domyślić się, że największym wyzwaniem będzie odczytanie tych wartości z wyrażenia – reszta pozostanie taka sama jak w poprzednim wpisie.

Zaczniemy zatem od następującej sygnatury:

public class RoutingAssert { public static void Verify<T>(string url, Expression<Action<T>> mapping) { } }

Z poprzedniego wpisu pamiętamy, że potrzebujemy zainicjalizować HttpConfiguration:

private static HttpConfiguration InitConfig() { var config = new HttpConfiguration(); config.MapHttpAttributeRoutes(); config.EnsureInitialized(); return config; }

W tej chwili, inicjalizujemy wyłącznie routing za pomocą atrybutów. 

Kolejny etap to stworzenie HTTPRequest:

private static HttpRequestMessage InitRequest(string url, HttpConfiguration config) { HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url); var routeData = config.Routes.GetRouteData(request); request.Properties[HttpPropertyKeys.HttpRouteDataKey] = routeData; return request; }

Następnie za pomocą selektora, możemy uzyskać deskryptor kontrolera:

private HttpControllerDescriptor GetControllerDescriptor(HttpConfiguration config, HttpRequestMessage request) { IHttpControllerSelector controllerSelector = new DefaultHttpControllerSelector(config); HttpControllerDescriptor controllerDescriptor = controllerSelector.SelectController(request); return controllerDescriptor; }

Chcemy również weryfikować akcje, zatem potrzebujemy ActionDescriptor:

private static HttpActionDescriptor GetActionDescriptor(HttpConfiguration config, HttpControllerDescriptor controllerDescriptor, HttpRequestMessage request) { var routeData = (IHttpRouteData)request.Properties[HttpPropertyKeys.HttpRouteDataKey]; HttpControllerContext controllerContext = new HttpControllerContext(config, routeData, request); controllerContext.ControllerDescriptor = controllerDescriptor; IHttpActionSelector actionSelector = new ApiControllerActionSelector(); HttpActionDescriptor actionDescriptor = actionSelector.SelectAction(controllerContext); return actionDescriptor; }

Korzystając z powyższych metod pomocniczych, główna metoda Verify aktualnie wygląda następująco:

public static void Verify<T>(string url, Expression<Action<T>> mapping) { HttpConfiguration config = InitConfig(); HttpRequestMessage request = InitRequest(url, config); HttpControllerDescriptor controllerDescriptor = GetControllerDescriptor(config, request); HttpActionDescriptor actionDescriptor = GetActionDescriptor(config, controllerDescriptor, request); }

Asercja kontrolera jest bardzo prosta i wystarczy:

Assert.That(controllerDescriptor.ControllerType, Is.EqualTo(typeof(T)));

Największe wyzwanie to oczywiście akcja, wraz z parametrami. Z tego względu warto stworzyć kolejną metodę pomocniczą o następującej sygnaturze:

private static void AssertAction<T>(Expression<Action<T>> mapping, HttpActionDescriptor actionDescriptor,HttpRequestMessage request)

Asercja nazwy akcji również jest dość prosta:

var expectedMethod = (MethodCallExpression)mapping.Body; Assert.That(actionDescriptor.ActionName,Is.EqualTo(expectedMethod.Method.Name));

Najwięcej wysiłku musimy włożyć w wyodrębnienie argumentów z wyrażenia. Przede wszystkim potrzebujemy:

HttpParameterDescriptor[] actualParameters = actionDescriptor.GetParameters().ToArray(); var httpRouteData = (IHttpRouteData[]) routeData.Values.First().Value;

Pierwsza kolekcja to lista deskryptorów dla parametrów. Tak samo jak ControllerDescriptor oraz ActionDescriptor, opisują one po prostu typy i nazwy przekazanych argumentów. RouteData pochodzi z URL i zawiera faktyczne wartości, które zostały przekazane. Za pomocą RouteData oraz ActualParameters będziemy w stanie odczytać przekazane parametry.

Spójrzmy teraz na najważniejszą metodę, AssertParameters:

private static void AssertParameters(IHttpRouteData routeData, HttpParameterDescriptor[] actualParameters, MethodCallExpression expectedMethod) { for (int i = 0; i < expectedMethod.Arguments.Count; i++) { var expectedValue = ((ConstantExpression)expectedMethod.Arguments[i]).Value; var actualValue = routeData.Values[actualParameters[i].ParameterName]; Assert.That(actualValue, Is.EqualTo(expectedValue.ToString())); } }

Zmienna expectedValue pochodzi z przekazanego wyrażenia (lambda), a actualValue z wspomnianego właśnie RouteData. Kod jest bardzo uproszczony i nie przewiduje np. DateTime czy złożonych parametrów. Podsumowując, cały helper wygląda następująco:

public class RoutingAssert { public static void Verify<T>(string url, Expression<Action<T>> mapping) { HttpConfiguration config = InitConfig(); HttpRequestMessage request = InitRequest(url, config); HttpControllerDescriptor controllerDescriptor = GetControllerDescriptor(config, request); HttpActionDescriptor actionDescriptor = GetActionDescriptor(config, controllerDescriptor, request); Assert.That(controllerDescriptor.ControllerType, Is.EqualTo(typeof(T))); AssertAction(mapping,actionDescriptor,request); } private static void AssertAction(Expression<Action<T>> mapping, HttpActionDescriptor actionDescriptor,HttpRequestMessage request) { var routeData = GetRouteDataFromRequest(request); var expectedMethod = (MethodCallExpression)mapping.Body; Assert.That(actionDescriptor.ActionName,Is.EqualTo(expectedMethod.Method.Name)); HttpParameterDescriptor[] actualParameters = actionDescriptor.GetParameters().ToArray(); var httpRouteData = (IHttpRouteData[]) routeData.Values.First().Value; AssertParameters(httpRouteData[0], actualParameters, expectedMethod); } private static HttpActionDescriptor GetActionDescriptor(HttpConfiguration config, HttpControllerDescriptor controllerDescriptor, HttpRequestMessage request) { var routeData = GetRouteDataFromRequest(request); HttpControllerContext controllerContext = new HttpControllerContext(config, routeData, request); controllerContext.ControllerDescriptor = controllerDescriptor; IHttpActionSelector actionSelector = new ApiControllerActionSelector(); HttpActionDescriptor actionDescriptor = actionSelector.SelectAction(controllerContext); return actionDescriptor; } private static IHttpRouteData GetRouteDataFromRequest(HttpRequestMessage request) { return (IHttpRouteData)request.Properties[HttpPropertyKeys.HttpRouteDataKey]; } private static HttpControllerDescriptor GetControllerDescriptor(HttpConfiguration config, HttpRequestMessage request) { IHttpControllerSelector controllerSelector = new DefaultHttpControllerSelector(config); HttpControllerDescriptor controllerDescriptor = controllerSelector.SelectController(request); return controllerDescriptor; } private static HttpRequestMessage InitRequest(string url, HttpConfiguration config) { HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url); var routeData = config.Routes.GetRouteData(request); request.Properties[HttpPropertyKeys.HttpRouteDataKey] = routeData; return request; } private static HttpConfiguration InitConfig() { var config = new HttpConfiguration(); config.MapHttpAttributeRoutes(); config.EnsureInitialized(); return config; } private static void AssertParameters(IHttpRouteData routeData, HttpParameterDescriptor[] actualParameters, MethodCallExpression expectedMethod) { for (int i = 0; i < expectedMethod.Arguments.Count; i++) { var expectedValue = ((ConstantExpression)expectedMethod.Arguments[i]).Value; var actualValue = routeData.Values[actualParameters[i].ParameterName]; Assert.That(actualValue, Is.EqualTo(expectedValue.ToString())); } } }

Jeśli metoda przyjmuje jako parametr wyrażenia lambda lub DateTime powyższy kod nie zadziała. Podobnie jest on ograniczony wyłącznie do HTTP GET. Myślę jednak, że taka próbka daje wystarczająco dużo informacji, aby rozszerzyć helper już we własnym zakresie.

ASP.NET WebAPI – testowanie routingu

Szczególnie w przypadku WebAPI, routing może być dość skomplikowany. W Nancy bardzo łatwo przetestować mapowanie między URL a zaimplementowaną logiką. W WebAPI moim zdaniem jest to dużo mniej wygodnie, ale wciąż powinniśmy zadbać o to, aby mieć zautomatyzowane testy dla routing’u.

Załóżmy, że mamy kontroler z niestandardowym routingiem:

public class SampleController : ApiController { [Route("site/{siteId}/person/{personid}/profile/words/{searchText}")] public int[] GetData(int siteId, int personId, string searchText) { return Enumerable.Range(1, 10).ToArray(); } }

Przykładowy, dopasowany URL to “http://localhost:35447/site/1/person/3/profile/words/pi”.

Możemy mieć przetestowaną całą logikę, zwracane kody HTTP, ale i tak bez testów routingu ryzykujemy bardzo dużo.

Zacznijmy od testu kontrolera. Chcemy upewnić się, że wywołanie powyższego linka, spowoduje zmapowanie do SampleController:

[TestFixture] public class RoutingTests { const string Url = "http://www.test.com/site/1/person/3/profile/words/ppp"; [SetUp] public void Setup() { } [Test] public void VerifyController() { var config = new HttpConfiguration(); config.MapHttpAttributeRoutes(); config.EnsureInitialized(); IHttpControllerSelector controllerSelector = new DefaultHttpControllerSelector(config); HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Url); var routeData = config.Routes.GetRouteData(request); request.Properties[HttpPropertyKeys.HttpRouteDataKey] = routeData; HttpControllerDescriptor descriptor = controllerSelector.SelectController(request); Assert.That(descriptor.ControllerType, Is.EqualTo(typeof (SampleController))); } }

Najpierw tworzymy instancję HttpConfiguration oraz wywołujemy MapHttpAttributeRoutes, ponieważ w powyższym przykładzie, użyliśmy atrybutów do konfiguracji routingu.

Następnie musimy zainicjalizować IHttpControllerSelector – klasę odpowiedzialną za wybranie kontrolera na podstawie URL.

W teście, korzystamy z HttpRequestMessage. Niezbędne jest zatem przekazanie danych routingu za pomocą:

var routeData = config.Routes.GetRouteData(request); request.Properties[HttpPropertyKeys.HttpRouteDataKey] = routeData;

Przejdźmy teraz do rozszerzenia powyższego testu, tak aby zawierał on również akcję:

[Test] public void VeryfiControllerAndAction() { var config = new HttpConfiguration(); config.MapHttpAttributeRoutes(); config.EnsureInitialized(); IHttpControllerSelector controllerSelector = new DefaultHttpControllerSelector(config); HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Url); var routeData = config.Routes.GetRouteData(request); request.Properties[HttpPropertyKeys.HttpRouteDataKey] = routeData; HttpControllerDescriptor controllerDescriptor = controllerSelector.SelectController(request); HttpControllerContext controllerContext = new HttpControllerContext(config, routeData, request); controllerContext.ControllerDescriptor = controllerDescriptor; IHttpActionSelector actionSelector = new ApiControllerActionSelector(); HttpActionDescriptor actionDescriptor = actionSelector.SelectAction(controllerContext); Assert.That(actionDescriptor.ActionName, Is.EqualTo("GetData")); Assert.That(controllerDescriptor.ControllerType, Is.EqualTo(typeof(SampleController))); }

Widzimy, że sprawa trochę się skomplikowała, ale zasada jest analogiczna – potrzebujemy dodatkowo dostępu do IHttpActionSelector.

Powyższy kod jest zdecydowanie zły i brzydki. W następnym w poście napiszemy wrapper, który umożliwi nam korzystanie z tego w produkcyjnym kodzie. Ponadto, to nie jest jedyny sposób testowania rountingu w ASP.NET i zajmiemy się  również innymi podejściami.

ASP.NET MVC 6.0 – ViewComponents

W ASP.NET MVC 6.0 (ASP.NET 5.0) usunięto metodę Html.Action. Służyła ona głównie do generowania widoków częściowych. Na przykład, jeśli jakąś funkcjonalność mieliśmy na każdej stronie, wtedy warto było stworzyć child-action i renderować osobno każdą część. Tworzyliśmy wtedy osobny widok i akcję  w kontrolerze. Główny widok wywoływał daną akcję (child-action) na kontrolerze i renderował wskazaną część. Więcej o ChildAction można poczytać tutaj.

Dlaczego zatem Html.Action został usunięty? Zdecydowano, że  stare rozwiązanie nie było zbyt intuicyjne… Osobiście nie miałem z nim większych problemów, aczkolwiek ViewComponents jest również dobrym podejściem.

Załóżmy, że chcemy wyświetlać baner na każdej podstronie. Dla uproszczenia, nasz baner będzie po prostu zawierał aktualny czas:

public class BannerViewComponent:ViewComponent { public IViewComponentResult Invoke(int itemsCount) { DateTime[] data = new DateTime[itemsCount]; for (int i = 0; i < itemsCount; i++) data[i] = DateTime.Now; return View(data); } }

Zwracamy zawsze IViewComponentResult i metoda powinna nazywać się Invoke.  Z kolei typ i liczba parametrów są dowolne. W naszym przypadku przekazywany jest parametr itemsCount. Mamy tutaj pełną dowolność. Kolejny etap to stworzenie widoku, wyświetlającego przekazaną listę DateTime:

@model System.DateTime[] <h3>Sample</h3> @foreach (var dateTime in Model) { <p>@dateTime</p> }

W widoku nie ma nic nadzwyczajnego. Czysty szablon razor.  Należy jednak pamiętać, aby nazwać go Default.cshtml i umieścić w folderze Views/{nazwa kontrolera}/Components/Banner.

W głównym widoku z kolei wywołujemy po prostu:

@Component.Invoke("Banner", 5);

Drugi parametr to ItemsCount, który służy nam jako parametr wejściowy do ViewComponent. Myślę, że kod wygląda bardziej przejrzyście niż w przypadku Action, ale nie stanowi dla mnie to jakieś wielkiej różnicy. Intencja jest bardziej jasna z ViewComponent, ale moim zdaniem usunięcie starej metody spowoduje więcej problemów niż korzyści.

ASP.NET MVC 6 Tag Helpers

Tag Helpers mają na celu ułatwić tworzenie widoków. Cel jest taki, aby widok jak najbardziej przypominał czysty plik HTML. Pierwszym etapem było wprowadzenie Razor, a teraz w MVC 6 mamy tzw. Tag Helper. W celu przetestowania tego samemu, należy zainstalować najpierw Visual Studio 2015.

Zacznijmy od przykładu. Klasyczny sposób na definiowanie linku to:

@Html.ActionLink("Jakis tekst", "About","Home",null,new { @class="styles1"})

Jeśli preferujemy składnię bardziej zbliżoną do HTML można byłoby także:

<a href="@Url.Action("About","Home")" cl class="styles1">Jakis tekst</a>

Oba rozwiązania nie są jednak idealne. Pierwsze kompletnie nie przypomina HTML, a drugie jest dość toporne. Używanie Url.Action jako parametr href jest mało wygodne.

Dzięki AnchorTagHelper możemy teraz:

<a asp-controller="Home" asp-action="About" class="styles1">Jakis tekst</a>

Oprócz tego, istnieje wiele innych helper’ow, np. InputTagHelper:

<input asp-for="StartDate" asp-format="{0:yyyy-MM-dd}" />

InputTagHelper ma dwa atrybuty, asp-for oraz asp-format. Pierwszy służy do określenia pola w modelu, a drugi formatu, w którym powinna wyświetlić się wartość. Klasyczny sposób wyglądałby następująco:

@Html.TextBoxFor(model => model.StartDate)

Do dyspozycji mamy również LabelTagHelper:

<label asp-for="FirstName" />

Nie chcę pokazywać przykładów dla każdego helpera bo wyglądają one analogicznie do siebie. Dla zainteresowanych wspomnę tylko, że mamy jeszcze SelectTagHelper, TextAreaTagHelper, ValidationMessageTagHelper, ValidationSummaryTagHelper oraz FormTagHelper.

Na zakończenie formularz:

<form asp-action="Submit" asp-controller="Home" asp-anti-forgery="true" method="post"> <div> <label asp-for="FirstName" /> <input asp-for="FirstName" type="text" /> <span class="validation" asp-validation-for="FirstName" /> </div> <strong>Validation Errors</strong> <div class="validation" asp-validation-summary="ModelOnly" /> </form>

Moim zdaniem ładniej to wygląda niż mieszanie C# z HTML, jak to było we wcześniejszych wersjach.

Circuit Breaker: Implementacja za pomocą Polly

W poprzednim poście wyjaśniłem na czym polega działanie wzorca i kiedy z niego korzystać. Dzisiaj przyszedł czas na implementację. Nie będziemy jednak pisać wszystkiego od początku, ponieważ jest to dość skomplikowane i prawidłowa implementacja zajęłaby sporo czasu. Kilka postów wcześniej, pisałem o Polly, jako mechanizmie do powtarzania nieudanych operacji. Zachęcam do przeczytania tego wpisu przed dalszą lekturą. Dla przypomnienia tytko, wkleję najprostszy przykład:

class Program { static void Main(string[] args) { var policy = Polly.Policy.Handle<DivideByZeroException>().Retry(); policy.Execute(DoSomething); Console.ReadLine(); } public static void DoSomething() { Console.WriteLine("Do something"); throw new DivideByZeroException(); } }

Dzięki Polly,  w analogiczny sposób możemy skonfigurować nasz “obwód”.:

Policy policy = Policy.Handle<Exception>(). CircuitBreaker(3, TimeSpan.FromMinutes(1));

Pierwszy parametr to liczba dopuszczalnych niepowodzeń, a drugi to na ile czasu obwód zostanie otwarty. Całość naszego eksperymentu może wyglądać następująco:

class Program { static void Main(string[] args) { Policy policy = Policy.Handle<Exception>(). CircuitBreaker(3, TimeSpan.FromMinutes(1)); Run(policy); Run(policy); Run(policy); Run(policy); Console.ReadLine(); } private static void Run(Policy policy) { try { policy.Execute(() => DoSomething()); } catch (TimeoutException e) { Console.WriteLine("Nieudana proba."); } } public static void DoSomething() { Console.WriteLine("Test"); throw new TimeoutException(); } }

Uruchamiamy metodę 4 razy, zatem za czwartym dostaniemy wyjątek:

image

Wystarczy, że odczekamy minutę przed czwartym wywołaniem i jak spodziewamy się, obwód pozostanie zamknięty. Kod:

Policy policy = Policy.Handle<Exception>(). CircuitBreaker(3, TimeSpan.FromMinutes(1)); Run(policy); Run(policy); Run(policy); Thread.Sleep(TimeSpan.FromMinutes(1)); Run(policy);

image

Obsługa zdalnych wywołań: wzorzec Circuit Breaker

Sporo ostatnio o SOA i mikroserwisach. Jednym z wyzwań podczas rozłupywania monolitu na serwisy jest wydajność. Wywołania in-memory są zastępowane np. HTTP lub innym zdalnym protokołem. Niesie to ze sobą kilka niedogodności m.in.:

1. Wydajność jest dużo mniejsza – serializacja, deserializacja, nawiązanie połączenia, transmisja danych.

2. Serwis może być nieaktywny.

3. Może wystąpić timeout.

Powyższe punkty mogą być wyjątkowo niebezpieczne, gdy wiele usług próbuje wywołać serwisy, które aktualnie nie odpowiadają. Każde wywołanie niesie ze sobą zużycie zasobów. Jeśli zbyt długa kolejka nieudanych wywołan zostanie skumulowana, kolejna usługa może przestać odpowiadać. Przypomina to efekt domina, gdzie jedna usługa próbuje nieustannie połączyć się z drugą, która aktualnie nie odpowiada. Przez zbyt dużą liczbę nieudanych połączeń, taka usługa sama przestaje odpowiadać, powodując takie same efekty na innych usługach.

Wzorzec Circuit Breaker, ma na celu zminimalizowanie efektu, gdy część usług nie odpowiada, a wiele klientów chce do nich uzyskać dostęp. Nie ma sensu stosować wzorca, gdy wiemy, że nie mamy dużej ilości wywołań na sekundę. W takiej sytuacji, timeout nie zaszkodzi i nie spowoduje wyczerpania zasobów.

W przypadku, gdy mamy setki wywołań, wtedy lepiej, gdy np. pierwszy klient ostrzeże innych o tym, że i tak nie ma sensu łączyć się z daną usługą, ponieważ ona nie odpowiada. I to jest właśnie cała istota wzorca. Stanowi on punkt między konsumentami, a usługą. Jeśli wszystko jest OK to obwód jest zamknięty (circuit). W momencie, gdy nastąpi timeout lub inna zdefiniowana awaria, wtedy obwód jest otwierany tak, że inni klienci dostaną natychmiastową odpowiedź, że nie można połączyć się z daną usługą. Nie będziemy musieli czekać na timeout, co pochłania ogromne ilości zasobów (w przypadku akumulacji połączeń).

Kolejny problemy jaki należy rozwiązać to ponowne zamknięcie obwodu po naprawie awarii. W przypadku programów, bardziej racjonalne jest automatyczne zamknięcie niż czekanie na manualną interwencję. Sposobów jest wiele, ale najprostszym może być okresowe zamykanie obwodu. Jeśli po automatycznym zamknięciu, znów okaże się, że zewnętrzne zasoby są niedostępne, wtedy możemy go natychmiast otworzyć i czekać na kolejny moment, kiedy możemy spróbować i przetestować połączenie poprzez zamykanie obwodu.

Wzorzec zatem, to nic innego jak proxy, który ma następujące stany:

1. Obwód zamknięty – normalny tryb pracy. Wszystkie zapytania są przesyłane do danych usług\dostawców. W tym stanie, proxy powinien również śledzić liczbę nieudanych połączeń. Jeśli przekroczy ona z definiowany próg w danym okresie (np. 5 nieudanych połączeń w ciągu 10 sekund), wtedy obwód jest otwierany. Warto podkreślić, że chcemy resetować licznik niepowodzeń okresowo. Mam na myśli, że nie powinniśmy po prostu śledzić całkowitej liczby błędów. Chcemy uniknąć sytuacji, gdzie tymczasowe awarie po paru dniach pracy mogą doprowadzić do otwarcia obwodu. Dzięki prostej zasadzie, że awarie muszą nastąpić z określoną częstotliwością, unikniemy otwarcia obwodu w przypadku krótkotrwałych awarii albo po prostu chwilowego przeciążenia sieci.

2. Obwód otwarty – jeśli proxy jest w tym stanie, to znaczy, że dopuszczalna liczba nieudanych połączeń została przekroczona i ze względu bezpieczeństwa, obwód został otwarty. Wszystkie wywołania kończą się natychmiastowym wyjątkiem, bez pochłaniania zasobów (jeszcze raz podkreślam, w celu uniknięcia kaskadowego wyczerpania zasobów).

3.  Powyższe dwa stany są obowiązkowe w każdej implementacji. Zwykle jednak dodaje się kolejny, który jest pomiędzy nimi (half-open, półotwarty). Wspomniałem wyżej, że nie chcemy manualnie zamykać obwodu po awarii. Prostym rozwiązaniem jest dopuszczanie ograniczonej liczby zapytań, które będą stanowić tak naprawdę test usług. Jeśli  zakończą się powodzeniem, wtedy możemy przypuszczać, że awaria została naprawiona i możemy dopuścić już wszystkie zapytania poprzez całkowite zamknięcie obwodu.

W przyszłym wpisie pokażę prostą implementację wzorca w C#.  Wszystko zależy od potrzeb i w zależności od tego, implementacja wzorca może być bardziej lub mniej skomplikowana. Zdecydowanie należy wziąć pod uwagę wielowątkowość i wynikające z tego zagrożenia typu stampede czy lock convoy.

Kolejnym ważnym aspektem jest wykonywanie logów. W końcu jest to doskonały punkt na wszelkiego typu monitoring, audyt, logging itp. Zdecydowanie stanowi to dobry punkt do czerpania informacji o sposobie wykorzystania naszego systemu.

Innym wyzwaniem jest łapanie konkretnych wyjątków. Nie chcemy wszystkich traktować jednakowo bo choćby po statusie HTTP, możemy domyślić się czy awaria potrwa kilka minut czy godzin. Bardzo możliwe, że same wysłanie zapytania zakończyło się błędem i w tym przypadku, jakakolwiek próba wysłania kolejnego zapytania nie ma sensu.

Istnieje wiele strategii zamykania obwodu po awarii. Może być to np. okresowe zamykanie, ping’owanie usługi w celu sprawdzenia jej stanu czy wspomniany mechanizm half-open.

Visual Studio 2015 RC– network

Niedawno pojawiła się wersja RC VS 2015. Wraz z nią, dodano nowe narzędzie diagnostyczne.  O  performance hub pisałem już wielokrotnie. Visual Studio coraz więcej narzędzi zewnętrznych wbudowuje w IDE. Mamy zatem już do dyspozycji profilery jak i o RC, diagnozowanie ruchu sieciowego.

Więcej szczegółów znajduje się tutaj:

http://blogs.msdn.com/b/visualstudio/archive/2015/05/04/introducing-visual-studio-s-network-tool.aspx

Załóżmy, że mamy prosty kod, który wykona połączenie HTTP:

HttpClient client = new HttpClient(); var response = client.GetAsync("http://www.pzielinski.com").Result; var content = response.Content.ReadAsStringAsync().Result; Console.WriteLine(content);

Dzięki nowemu narzędziu, będziemy wstanie prześledzić wszystkie połączenia tak jak to w przypadku zewnętrznych narzędzi typu Fiddler. Jest to bardzo przydatne w diagnozowaniu usług REST, które są dzisiaj wyjątkowo popularne.

Niestety nie udało mi się uruchomić Performance Hub Network na własnym komputerze ani nawet na Azure VM. Wszystkie próby kończyły się “diagnostics session failed to start “, co jest dosyć słabe jak na wersje RC… W poście posłużę się więc screenami z MSDN, które również znajdują na dodanym linku na początku postu.

Z menu głównego wybieramy zatem Debug –> Start Diagnostic Tools without debugging i zaznaczamy network:

image

Po naciśnięciu “Start”, zobaczymy najpierw okno Network Summary (źródło screenu MSDN Blogs):

Widzimy w oknie listę połączeń, zwróconych kodów, czasów wykonania itp. Jak to bywa w VS, okno jest interaktywne i możemy np. posortować wyniki oraz dostosować widok do naszych potrzeb. Aby nie przegapić najgroźniejszych kodów HTTP tzn. 4xx oraz 5xx, zostają one zaznaczone na czerwono (źródło screenu MSDN Blogs):

Możliwe jest również zapisanie danych do pliku i co najważniejsze wspierany jest format .har.  Dzięki temu możemy przeanalizować ruch w bardziej zaawansowanych narzędziach typu Fiddler. Przykładową zawartość pliku .har można znaleźć np. tutaj.

Klikając dwukrotnie na danej pozycji, dostaniemy dodatkowe informacje, takie jak pełna odpowiedź i zapytanie:

8321.BUILD2015_2D00_NetworkTool_2D00_Details-Panel.PNG (586×713)

Narzędzie nie dodaje nic nowego w porównaniu do tego, co było dostępne cały czas w Fiddler. Moim zdaniem jednak, dobrze, że takie narzędzia integrowane są z IDE. W erze usług RESTful, takie zadania jak podglądanie statusów HTTP są bardzo częste więc dobrze mieć je pod ręką.

Kompatybilność usług: consumer-driven contracts

Ostatnio na blogu sporo o SOA. W poprzednim wpisie, poruszyłem temat kompatybilności, teraz czas na coś, co ma na celu zminimalizowanie ilości różnych wersji usług. Najprostszym sposobem na uniknięcie problemów z kompatybilnością jest po prostu nie wprowadzenie niekompatybilnych zmian.

Consumer-driven contracts to prosty sposób, aby mieć pod kontrolą śledzenie zmian oraz ich wpływ  na konsumentów naszej usługi.

Zwykle usługi posiadają pewien schemat (schema) czyli po prostu kontrakt. Najbardziej restrykcyjną polityką jest dopasowywanie kontraktu posiadanego przez klienta z tym dostępnym aktualnie po stronie serwera. Jeśli nie są one takie same, wtedy uznawane jest, że klient jest niekompatybilny z serwerem.

Powyższa strategia jest nieco zbyt restrykcyjna. W końcu dodanie nowej metody do API nie powinno mieć wpływu na klientów. Dlatego zwykle uznaje się, że schemat (schema) można rozszerzać o nowe wpisy czy pola. Jeśli np. encja Person zawiera FirstName oraz LastName, wtedy dodanie nowej właściwości do niej (np. MiddleName) nie powinno mieć znaczenia. W końcu taka zmiana, nie powinna zaszkodzić aktualnym klientom.

Co jednak z usunięciem pola? Taka zmiana w standardowym SOA jest uznawana za łamiącą kompatybilność i w przypadku semantycznego wersjonowania, powinno zwiększyć się segment “Major”.  Jest to oczywiście naturalne i logiczne ponieważ istnieje ryzyko, że ktoś korzysta np. z FirstName i usunięcie tego pola jest niedopuszczalne.

Consumer-driven contracts wychodzą  z założenia, że często nawet usunięcie pola nie powinno wiązać się ze zmianą “major”. Jeśli nikt z niego aktualnie nie korzysta, to po co tym martwić się? Wystarczy, że będziemy wiedzieć jak nasz serwis jest używany. Konsumenci kształtują nasze API, stąd nazwa consumer driven contracts.

Klienci definiują zatem ich wymagania za pomocą testów jednostkowych. Test jednostkowy może sprawdzać czy dane pole występuje w schema.

Następnie serwis bierze napisane testy przez konsumentów i wykonuje je (jako część CI) na realnym serwisie. Jeśli jakieś pole zostało usunięte, a było wykorzystywane przez jednego z konsumentów, test zakończy się oczywiście błędem. Po stronie serwisu możemy zatem dokonać jakichkolwiek zmian (usunięcie, dodanie nowych pól itp) dopóki żaden z testów “konsumenckich” nie zostanie popsuty.

Konsumenci zwykle piszą klasyczne testy jednostkowe, które wykonywane są na mock’u, a nie realnym serwisie. Serwis z kolei, zbiera wszystkie testy konsumentów i wykonuje normalny, integracyjny test. Jakakolwiek modyfikacja API jest teraz bezpieczna i przewidywalna jeśli chodzi o rezultat.

W jaki sposób piszemy testy to już zależy od konkretnego API. Możemy sprawdzać XML\JSON czy dane pole istnieje. W zależności od konkretnego protokołu inaczej to będzie wyglądać. Cel jest zawsze ten sam – wyrazić jakie pola i elementy są wymagane dla danego konsumenta i ich zmiana musi zakończyć się wydaniem nowej wersji ze zmianą “major”.

Kompatybilność usług

Kompatybilność usług jest problemem w każdej architekturze SOA, ale w przypadku mikro-usług staje się jeszcze bardziej widoczna. W przyszłości chce napisać post o tzw. consumer-driven contracts, które znacząco mogą zminimalizować potrzebę wersjonowania usług. W każdym razie, bardzo prawdopodobne, że w pewnym momencie zajdzie potrzeba wprowadzenia zmiany, która nie jest kompatybilna wstecz.

Jeśli nasz system składa się np. z 20 usług to musimy mieć mechanizm, który zagwarantuje nam, że nie wprowadzimy zmiany w którejś z usług, która spowoduje, że całość przestanie działać. Jednym z rozwiązań jest tzw. semantyczne wersjonowanie (semantic versioning).

Idea jest bardzo prosta. Każda z usług jest opisana wersją w następującym formacie:

Major.Minor.Patch (przykład 3.5.15)

Major to najbardziej dotkliwa zmiana wersji i dotyczy funkcjonalności, która nie jest kompatybilna z poprzednimi wersjami. Usługa 3.5.16 nie jest kompatybilna z 2.5.16.

Minor oznacza nową funkcjonalność, ale jest ona kompatybilna z poprzednimi wersjami. Może to dotyczyć zatem dodania nowej metody do usługi, co naturalnie nie powinno mieć znaczenia dla starszych klientów.

Patch dotyczy drobnych zmian typu naprawienie błędu, refaktoryzacja itp.  Naturalnie jest to najbezpieczniejsza zmiana ponieważ nie zmienia nawet API\kontraktu.

Proszę zauważyć, że nie możemy polegać na standardowym wersjonowaniu, gdzie każdy commit zmienia patch. Zwykle, w wiadomości każdego commit’a podaje się numer user story albo błędu. Następnie CI może na podstawie tego, zdecydować, który segment wersji należy zmienić (major, minor, patch).

Możliwe jest również doczepianie tagu, np. 3.5.15-prealpha albo 3.5.16-release. Dla dzisiejszego postu nie ma to jednak znaczenia.

Wiemy zatem, że każda usługa jak i klient powinien mieć wersję. Konsument takiej usługi powinien mieć test jednostkowy sprawdzający jego wersje klienta z aktualnie dostępną wersją usługi. Jeśli wersja major konsumenta i usługi są różne, to wykonanie takiego testu powinno zakończyć się błędem. Dzięki temu, mamy pewność, że usługi w systemie są ze sobą kompatybilne i w momencie niekontrolowanej zmiany wersji, zostanie to wykryte w CI. Bez takich testów jednostkowych i semantycznego wersjonowania byłoby ciężko mieć kontrolę nad wszystkimi usługami i każda zmiana niosła by za sobą ryzyko, że coś zostanie popsute.