piątek, 20 lutego 2009

Mix SpringTestContext Framework i EasyMock

W ramach aktualnego projektu zastanawiałem się nad zagadnieniem jego testowania. Staram się aby projekt był prowadzony zgodnie z TDD i do tego celu korzystamy intensywnie z EasyMock oraz Spring TestContext Framework. Wszystko działa jak należy, ale pojawił się problem z testowaniem integracyjnym (przy wykorzystaniu Spring TestContext Framework), gdy ścieżka wykonania testu obejmuję komponenty, które to są zależne od innych (dostarczonych z zewnątrz) elementów.

Zgodnie z best-practices, w ramach projektu context Springowy zostaly podzielony na oddzielne pliki nie tylko ze względu na warstwę systemu(web,service,DAO, adapter), ale także ze względu na infrastrukturę (Junit, Tomcat, Websphere).

Początkowo wydawało się, że wystarczy stworzyć dodatkowy plik contextu - tylko na potrzeby testów- który mógłby defniować beany odpowiadające systemom zewnętrznym. W takim przypadku należy stworzyć klasy tych beanów, które pełniłyby rolę stubów. Podejście takie ma 1 zasadniczą wadę: w przypadku gdy nasza logika związana z interakcją z systemem zewnętrznym jest bardziej skomplikowana niż "fire 'n forget"to możliwość definiowania zachowania stuba jest ograniczona. Logika działania stuba jest zdefiniowana "a priori" w jego klasie i ewentualna zmiana zachowania wiąże się z utworzeniem nowej klasy/rozbudową istniejącej. Osobiście nie jestem zwolennikiem tworzenia dużej ilości dodatkowego kodu wyłącznie na potrzeby testów, tzn. czasami nie da się od tego uciec, ale jeżeli istnieje alternatywne rozwiązanie...
Trywialny przykład interfejsu do systemu zewnętrznego:
public interface UserAuthenticationManager {

int logUser(String name, String pass);

}

Wartość funkcji logUser jest ściśle określona, ale dla uproszczenia przyjmijmy, ze interesują nas 4 przypadki:
nie ma usera o danej nazwie, name/pass nie pasują, user zablokowany, wymagana zmiana hasła usera. Każda z takich sytuacji jest mapowana na inną wartość liczbową i na tej podstawie następuję specyficzne przetwarzanie.
Stosując podejście przedstawione powyżej, możemy albo utworzyć 4 klasy stubów, umieścić je w osobnych plikach contextu i odpowienio do scenariusza ładować jeden z nich, albo utworzyć 1 klasę stuba a w niej umieścić odpowiednie "if" aby pokryć wszystkie 4 przypadki.
Oba podejścia są słabe: albo rozdrabniamy konfiguracje, co powoduję niesamowity przyrost plików konfiguracyjnych, albo zajmiemy się pisaniem logiki, która na podstawie odpowiednich wartości (wartości parametrów metody, albo wartości umieszczonej w zmiennej ThreadLocal) zwróci nam wartość odpowiednią do scenariusza jaki w tym momencie testujemy.

Idealne w sytuacji wydaje się połączenie Spring TestContext Framework oraz EasyMock. Scenariusze testów integracyjnych byłyby sterowane przez Spring, a w przypadku odwoływania się do systemów zewnętrznych do akcji wchodziłyby mocki, dla których zachowanie zostałoby określone(nagrane) bezpośrednio w metodzie testowej. Z technicznego punktu widzenia chodzi o to, aby móc podmieniać w czasie wykonywania testu wybrane beany Springowe (w moim przypadku singletony), na utworzone mocki.
Pierwszą przeszkodą było agresywne inicjowanie beanów Springowych, które są singletonami. Problem polegał na tym, ze zanim metody testowe zostaną odpalone (a w nich miałaby się odbyć podmiana) context Springowy zostanie zaczytany, a beany będące singletonami utworzone. Pewnym obejściem, może być ustawienie atrybut default-lazy-init na true w elemencie <beans> w plikach contextu.
Pomimo, że rozwiązuje to problem, jest to sprzeczne z filozofią związaną z agresywnym tworzeniem beanów, dzięki której pomimo dłuższego czasu startu aplikacji błędy związane z tworzeniem się contextu bardzo szybko "wylatują". Jest to szczególnie ważne przy uruchamianiu aplikacji w środowiskach integracyjnym i produkcyjnym. Dlatego zdecydowałem się znaleźc alternatywne rozwiązanie, dzięki któremu nie musiałbym "naginać"konfiguracji Springowej do testowania.
Szczęśliwie twórcy Spring TestContext Framework umozliwiają bardzo prosto określenie klasy, która to będzie odpowiedzialna za odczytywanie i ładowanie ApplicationContext a sprowadza się to do podania odpowiedniej klasy w annotacji ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"/context.xml"}, loader=LazyContextLoader.class)
public class Test{
...
}

Klasa LazyContextLoader jest odpowiedzialna za wczytywanie kontekstu i jej implementacja jest niemal identyczna do klasy GenericXmlContextLoader:
public class LazyContextLoader extends AbstractGenericContextLoader {

@Override
protected BeanDefinitionReader createBeanDefinitionReader( final GenericApplicationContext context ) {
XmlBeanDefinitionReader result = new XmlBeanDefinitionReader( context );
result.setDocumentReaderClass( LazyInitByDefaultBeanDefinitionDocumentReader.class );
return result;
}

@Override
public String getResourceSuffix() {
return "-context.xml";
}
}

Zmieniona została tylko implementacja interfejsu BeanDefinitionDocumentReader, który to odczytuję pliki XML z defincjami beanów.
Zadaniem LazyInitByDefaultBeanDefinitionDocumentReader jest ustawienie w locie na root każdego z zaczytanych plików XML atrybutu default-lazy-init na wartość true
public class LazyInitByDefaultBeanDefinitionDocumentReader extends DefaultBeanDefinitionDocumentReader {

@Override
protected BeanDefinitionParserDelegate createHelper( XmlReaderContext readerContext, Element root ) {
root.setAttribute( BeanDefinitionParserDelegate.DEFAULT_LAZY_INIT_ATTRIBUTE,"true" );
return super.createHelper( readerContext,root );
}
}

W ten sposób udało się "czysto" zaczytać konfiguracje Springowa bez inicjalizacji beanów.
Następnym krokiem pozostała podmiana istniejących beanów, która okazała się dość prosta:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"/context.xml"}, loader=LazyContextLoader.class)
public class Test{

@Autowired
private GenericApplicationContext context;

@Test
@DirtiesContext
public void testMethod(){
context.removeBeanDefinition( "userAuthenticationManager" );
UserAuthenticationManager mock = EasyMock.createMockUserAuthenticationManager.class );
EasyMock.expect( mock.logUser( (String)EasyMock.notNull(),(String)EasyMock.notNull() ) ).andReturn( 12 );
EasyMock.replay( mock );
context.getBeanFactory().registerSingleton( "userAuthenticationManager", mock );
...
}

Należy pamiętać jednak o kilku sprawach:
  • beany, które chcemy podmienić nie mogą być bezpośrednio injectowane w klasie testów, ani też nie moga być składowymi innych beanów, które mają byc injectowane (poprzez wkorzystywanie @Autowired)
  • metody testowe, które zmieniają context powinny byc oznaczone annotacją @DirtiesContext
  • Przedstawiony powyżej kod, wrzuca bean do contextu jako w pełni zainicjalizowany komponent, tzn. nie są na nim odpalane jakiekolwiek metody związane z cyklem życia beana, nie podlega konfiguracji AOP zawartej w plikach contextu.
  • Dla mojego projektu wystarczyło podmieniać singletony, nie potrzebowałem podmieniać beanów o innym cyklu życia
  • "leniwa" inicjalizacja beanów pozwala zaczytać niepełny context Springowy, czyli przykładowo taki, który nie musi zawierać definicji wszystkich beanów. Dopóki nie odwołamy się, za pomocą context.getBean() lub @Autowired, do beana, który nie został zdefiniowany, lub nie zostały zdefiniowane dla niego wszystkie zależne beany, możemy dowolnie "mieszać" w konfiguracji
  • Spring cachuje zaczytany context/contexty aplikacji na podstawie wartości locations
    annotacji @ContextConfiguration. Ze względów wydajnościowych sensownie jest wydzielić testy integracyjne, które w swoim działaniu mogą wywoływać systemy zewnętrzne od pozostałych testów integracyjnych systemu. Wydzielenie te będzie jedynie poprzez określenie dodatkowego (sztucznego) pliku contextu aby pozostałe testy integracyjne (te, które nie dotykają systemów zewnętrznych) mogły swobodnie korzystać z cachowanego contextu.

sobota, 24 stycznia 2009

Scaling Hibernate: tips, recipes and new perspectives oraz Tune this!

Po godzinnej przerwie rozpoczęły się sesję BOF. Jak się później okazało wszystkie sesje BOF prowadzone były w 2 wąskich, dusznych salkach ze słabym sprzętem audio-video.



Scaling Hibernate: tips, recipes and new perspectives - zgodnie z zapowiedziami Emmanuel Bernard miał przedstawić sposoby optymalizacji aplikacji opartych o Hibernate. Po minutowym wstępie, w którym omówił "klasyczne" metody optymalizacji: batch size czy fetch size ciut więcej można się było dowiedzieć o 2nd level cache:

  • nie ma sensu używać 2nd level cache dla bardzo dużych woluminów danych: jak mamy milion userów w systemie to nie ma sensu cachowac takiej ilości danych: skończy się to Out of memory exception lub sytuacją w której tylko część danych tak naprawdę będzie w cache i to nie koniecznie te, które byśmy chcieli
  • ma być już możliwe używanie JBoss Cache jako transakcyjnego 2nd level cache - miało zostać to ostatnio naprawione

Pojawiła się wzmianka o użyciu Hibernate w systemach masowego przetwarzania/wsadowych i korzystaniu z takich dobrodziejstw jak: batch insert, batch update, ScrollableResults, StatelessSession (pobierane encje są automatycznie odłączane i mogą zostac usunięte przez GC). Ja osobiście dodałbym do tego bezpośrednią możliwość wykonania operacji update/delete z poziomu Hibernate za pomocą session.createQuery("...").executeUpdate();
Następnie przedstawiono różne możliwości radzenia sobie w bardziej specyficznych przypadkach, np. bardzo duża ilość danych, bardzo duże obciązenie baz danych, dane z różnych (politycznych) względów muszą być rozdzielone.
Jeśli chodzi o seperacji danych to wyróżniamy następujące opcje:

  • najprostszą możliwością (choć nie do końca można to nazwać separacją) jest separacja logiczna: wszystkie dane są przechowywanych w tych samych tabelach a separacja odbywa się na poziomie aplikacyjnym(logicznym) za pomocą Hibernate Filter lub jakiś innych tricków. Od razu przypomniał mi się mechanizm przedstawiony przez Alefa Arendsena podczas Spring ONE. Chodziło o to, aby za pomocą Spring AOP wrzucić do sztucznej tabeli identyfikator aktywnej transakcji oraz identyfikator klienta. Encje nie zostały "zbindowane" do tabel a do widoków, których zadaniem jest filtrowanie danych zawartych w tabeli po identyfikatorze klienta, którego aktualna wartość (wartość do "zmatchowania") znajduje się w sztucznej tabeli
  • można rozproszyć dane na wiele schematów i są tutaj 2 podejścia: 1 SessionFactory na 1 schemat (problem skalowalności) lub 1 SessionFactory dla wszystkich schematów (super login, wymaga przepisania zapytań - uwzględnienie nazwy schematów lub implementacji metody onPrepareStatement interfejsu org.hibernate.Interceptor - doklejenie do zapytania nazwy schematu). Ma tu się pojawiać dodatkowo problem z 2nd level cache, ale nie jestem pewien czy dobrze rozumiem dlaczego
  • wykorzystanie mechanizmów bazy danych, szczególnie kuszącym rozwiązaniem jest Oracle VPD (filtr na poziomie bazy danych)

Jeśli dodatkowo danych jest tak dużo i zdecydujemy się je rozpraszać pomiędzy wiele baz danych to Emmanuel Bernard zaprezentował 2 najpowszechniejsze rozwiązania tego problem, oba dotyczące warstwy apikacyjnej:

  • Homogenous nodes - każda instancja serwera aplikacyjnego ma pulę połączeń do każdej z bazy danych. Podejście takie ma mie wiele problemów natury wydajnościowej: duża ilość połączeń do bazy danych, zużycie pamięci, wolny start aplikacji (na każdej instancji startuje tyle SessionFactory ile mamy baz danych)
  • Specialized nodes - poszczególne instancje serwera aplikacyjnego mają dostęp jedynie do określonych baz danych. Ważny jest tutaj odpowiedni mechanizm load-balancing, który musi rozrzucać żądania użytkowników na podstawie żądanej funkcjonalności. Zaletą takiego podejścia jest skalowalność oraz wykorzystanie zasobów, ale mogą pojawić się problemy gdy nie istnieje żadna instancja serwera posiadająca dostęp do wszystkich danych, których będzie wymagać żądanie użytkownika

Rozwiązaniem ma być Hibernate Shards, który jest mechanizmem partycjonowanie poziomego (horizonal partitioning), czyli każda z baz danych posiada ten sam model, a jedynie różne dane. Dla porównania, partycjonowanie pionowe (vertical partitioning) polega na podziale danych ze względu na funkcjonalność i każda baza danych ma inny model : na 1 trzymamy dane użytkowników, na 2 tabele produktów itd. Na poziomie architektury Hibernate Shards rozszerza bibliotekę Hibernate i stanowi warstwę, która enkapsuluje logikę wynikającą z rozproszenia danych pomiędzy wiele baz danych, a z technicznego punktu widzenia opakowuje wiele instancji SessionFactory (podłączonych fizycznie do różnych baz o tym samym modelu) dostarczając użytkownik ujednolicony sposób dostępu do danych. Twórcy biblioteki dokonują wielkich starań i zabiegów, aby do jej obsługi móc korzystać ze znanego i sprawdzonego Hibernate API, istnieją odpowiednie implementacje takich interfejsów jak: SessionFactory, Session, Query, Criteria. Dodatkowo dochodzą do tego bardziej złożone sprawy wynikające ze specyfiki rozproszenia danych, które to zostały wyrażone za pomocą interfejsów:

  • ShardSelectionStrategy - decyduje o tym, na której partycji utworzyć obiekt
  • ShardResolutionStrategy - decyduje o tym na jakiej partycji szukać obiektu o zadanym kluczu głównym
  • ShardAccessStrategy - w jaki sposób wykonywać zapytania, które dotyczą więcej niz 1 partycji

Aktualnie Hibernate Shards udostępnia implementacje (lub kilka implementacji) każdego z tych interfejsów, ale oczywiście bardzo łatwo stworzyć własną.
Jeśli chodzi o zapytania to jak na razie Hibernate w ograniczonym zakresie wspiera Criteria Query (np. avg po poszczególnych partycjach) oraz nie wspiera HQL (problem z parserem). Dodatkowo dany graf obiektów nie może obejmować więcej niż pojedynczej partycji, co osobiście uważam za wielkie ograniczenie. Wyobraźmy sobie przypadek gdy tworzymy rich data model (nasze encje posiadają powiązania do innych encji a nie jedynie ich identyfikatory) i mamy powiązania do generycznych danych, np. kraje (których nie ma sensu rozpraszać). W takim przypadku jak na razie polecane jest mapowanie "nieobiektowe" takiej relacji, tabele krajów przechowywać na jednej z partycji a do szybszego dostępu do niej trzymać ją w cache w pamięci. W przyszłości planuje się zrobić pewnego rodzaju mechanizm replikacji, który to umożliwiałby spójne przechowywanie takiego typu danych w każdej z partycji a dzięki temu możliwe będzie skorzystanie z dobrodziejstw baz danych (ograniczenie foreign key) oraz Hibernate (rich data model).
Przewiduję się dodatkowo wiele problemów z organizacją/reorganizacja danych, aby przygotować je na rozproszenie. Dotyczy to nie tylko inicjalnego rozproszenia, ale co gorsza podziału istniejących juz partycji. Do tego celu stworzony został mechanizm virtual shards, który jest warstwą pośrednią pomiędzy Hibernate Shards a instancjami baz danych czyli partycjami fizycznymi. Podział na partycje wirtualne powinien być dokonany ze względu na wymagania biznesowe i brać pod uwagę "przyszłe potrzeby" systemu. Wiele partycji wirtualnych może początkowo zostać zbindowanych do pojedynczej partycji fizycznej. W razie potrzeby, w dalszej fazie istnienia systemu partycja fizyczna może zostać "przepięta" do dedykowanej dla siebie partycji fizycznej.
Wniosek z tej części prezentacji jest taki, aby próbować wszystkiego, by uniknąć rozpraszania swoich danych. Jednak gdy osiągniemy punkt, w którym nic więcej nie da się zrobić i pozostaje nam jedynie fizyczne rozproszenie danych, a dodatkowo używamy Hibernate to projekt Hibernate Shards na pewno jest w stanie nam pomóc. Następnie Emmanuel Bernard przeszedł do "swojego" tematu, czyli Hibernate Search i właśnie wydawanej ksiązki "Hibernate Search in Action", której jest współautorem.

Tune This! - to było już 2 spotkanie tego dnia z Kirkiem Pepperdine. Po minimalnym wstępie omawiającym miejsca, które mogą mieć negatywny wpływ na wydajność: aplikacja, JVM/OS, hardware oraz paru ogólnikowych wskazówkach związanych z wydajnością, przeszliśmy do konkretnych przykładów. Kirkiem Pepperdine przygotował aplikację, a widownia na podstawie różnych miar zużycia zasobów i wyników z profilera miała ocenić co jest przyczyną słabej wydajności aplikacji. W pierwszym przypadku mieliśmy do czynienia z bardzo dużym obciążeniem procesora. Proces javy nie był głównym konsumentem CPU, a najbardziej obciążał sam system operacyjny. Profiler wskazywał, ze większość wątków było w stanie "wait". Wszystko to oznaczało, że nasza aplikacja ma problemy z lockami. Dopiero po wysunięciu takiej propozycji Kirkiem Pepperdine pokazał nam kod źródłowy aplikacji i faktycznie, ze względu na niewłaściwe użycie synchronized aplikacja działała tak wolno. Po usunięciu problemu, czas działania aplikacji był juz dużo lepszy, jednak tym razem zużycie CPU przez proces java było bardzo wysokie. Powodem tego może być aplikacja sama w sobie lub też działanie GC. Z powodu braku czasu Kirk Pepperdine nie zaprezentował hpjmeter, które to uważa za wyśmienite narzędzie do analizy działania GC, a jedynie pokazał nam w jaki sposób za pomocą HPROF znaleźć, w którym miejscu kodu, aplikacja tworzy na stercie obiekty zajmujące największą ilość pamięci. Jego przykład był dość banalny i błąd polegał na bezsensownym tworzeniu obiektów typu String.

czwartek, 22 stycznia 2009

Visual VM oraz Hibernate Tools

Kolejną sekcją były prezentacje: Tools in Action

VisualVM - new extensible monitoring platform
- tak na prawdę nie ma o czym pisać, po prostu zostało przedstawione narzędzie i jakiś tam mały przykład. Prezentacja bardzo podobna do tej autorstwa Adam Dudczaka http://www.jug.poznan.pl/wp-content/uploads/2008/10/jmx.pdf. Nawet jeśli Kirk Pepperdine przedstawił jakieś tips'n tricks ja tego nie wyłapałem.

Making full use of Hibernate Tools - zaczęło się od przestrogi: "Don't overdo it". Max Rydahl Andersen pokazał działanie Hibernate Tools w JBoss Developer Studio. Podpowiadanie kodu w zapytaniach HQL i plikach .hbm, odpalanie zapytań bezpośrednio z IDE oraz duża ilość wizzardów do generacji różnych artefaktów wzbudziło zainteresowanie. Nie dało się ukryć, że nie wiele osób z widowni na bieżąco śledzi co się dzieje w Hibernate Tools.

Omówiono bardziej szczegółowo mechanizm generowania artefaktów za pomocą Hibernate Tools, a cały schemat jest przedstawiony na rysunku poniżej.


Centralną część stanowi meta model reprezentowany przez klasę org.hibernate.Configuration, i który to może zostać zbudowany na podstawie różnych źródeł: pliki .hbm, klasy java z adnotacjami (Hibernate lub JPA), połączenie JDBC do baza danych . Następnie meta model jest "obrabiany" za pomocą odpowiedniego exportera, który może generować określone artefakty, np. pliki .hbm, klasy JAVA z adnotacjami, schemat bazy danych oraz jego dokumentacje. Osobiście spodobała mi się wbudowana możliwość użycia biblioteki freemarker jako exportera - wystarczy jedynie stworzyć odpowiedni template. Dodatkowo Hibernate Tools daje możliwość dostępu do klas użytkownika z poziomu template, dzięki temu bardziej złożona logika generowania może zostać zawarta w javie a nie bezpośrednio w template.

Reszta prezentacji została poświęcona na różne aspekty związane z konfiguracją odczytywania meta modelu z bazy danych oraz mapowanie go do modelu Hibernate (reverse engineering strategy). Dostępna out-of-box strategia jest bardzo mocno konfigurowalna (nawet per tabela/kolumna), ale jeśli pojawią się specjalne wymagania to zawsze można stworzyć własną implementację.

Wszystko te mechanizmy są dostępne w JBoss Developer Studio lub JBoss Tools(plugin do eclipse) w postaci wizzardów, ale takze z poziomu anta. Max Rydahl Andersen zapewniał, że wszystko to co można "wyklikać" w JBoss Tools jest jednocześnie dostępne jako taski ant.