Jak szybko załadować sobie kontekst przed pracą

Niektórzy słyszeli pewnie o zjawisku, które nazywa się Cognitive Switching Penalty. W uproszczeniu chodzi w nim o to, że w momencie, gdy przełączamy się miedzy zadaniami, tracimy czas i skupienie, a rośnie nasz poziom stresu i frustracja (ciekawy wątek w temacie). Widać to dobrze na przykładzie liderów zespołów, którzy starają się też programować, a którym ciągle ktoś przerywa, i nie są oni w stanie skończyć prostych zadań, które zaplanowali sobie na najbliższy czas.

Napotykam na zbliżony problem zarówno wówczas, gdy zaczynam rano pracę, jak i wtedy, gdy siadam do programowania po południu. Wówczas załadowanie kontekstu z poprzedniego dnia może mi zająć kilka dobrych minut. Odkryłem sposób, który skraca czas ładowania kontekstu z poprzedniego dnia. Od razu zaznaczam, że najpewniej nie odkryłem go jako pierwszy, niemniej zrobiłem to samodzielnie.

Sposób jest następujący: zarówno w biurze, jak i w domu kończę pracę, zostawiając sobie nieprzechodzący test jednostkowy. Wówczas, gdy siadam po przerwie do kodu, od razu mam proste zadanie, które muszę zrobić: muszę zaimplementować kod, aby test zaczął przechodzić. Zaimplementowanie kodu, by test się zazielenił, może wtedy zająć trzy minut, może zająć pięć minut, i to wystarczy, bo po takim czasie mam już odpowiedni flow, który pozwala mi płynnie przechodzić do kolejnych zadań.

Podobnie skuteczne w ładowanie kontekstu z poprzedniego dnia jest zostawienie niedokończonego testu do już zaimplementowanego kodu produkcyjnego. (Kto zawsze pisze testy przed kodem, niech pierwszy rzuci kamień.)

Ta metoda nie sprawdza się tak dobrze w przypadku prac utrzymaniowych, jak w przypadku implementacji nowych funkcjonalności. Minusem jest też to, że kto w ogóle nie pisze testów, nie będzie w stanie jej zastosować.

Nie stosuję tego sposobu zbyt restrykcyjnie, ale gdy wiem, że do końca dnia pracy lub programowania w domu zostało mi około kwadransa, zaczynam planować, gdzie w obszarze, w którym właśnie pracuję, mógłby powstać nieprzechodzący test, od którego mógłbym zacząć następnego dnia.

Zasilanie danymi: procesory

Aby stworzenie Star Trek API w zaplanowanym kształcie miało sens, musiało znaleźć się źródło danych, które byłoby relatywnie kompletne i które umożliwiałoby dostęp do danych przez sensowne i legalne API. Tym źródłem jest Wikia, wraz z dwoma hostowanymi tam wiki – Memory Alpha i Memory Beta.

Wszystkie wiki na Wikii działają na oprogramowaniu MediaWiki, rozwijanym od wielu lat na potrzeby Wikipedii. MediaWiki udostępnia własne API – niezbyt ustandaryzowane, ani SOAP-owe, ani REST-owe. Na Bitbuckecie istnieje projekt Bliki engine, javowy klient, który, chociaż nie idealny, oszczędza dużo pracy przy kwerendowaniu API stron opartych na MediaWiki.

Samo zasilenie składa się z trzech zasadniczych kroków. Readery i writery opisałem w poście o Spring Batchu. W tym wpisie skupię się na przybliżeniu szczegółów implementacji procesorów.

Po pierwsze, trzeba zbudować zbiór stron, z których zostanie zasilony dany model. Wymaga to ręcznego przejrzenia kategorii, w których pogrupowane są strony na wiki. Przykładowo, aby zbudować listę stron postaci fikcyjnych, które zostaną przeparsowane w następnym kroku, należy pobrać strony z czterech kategorii, z uwzględnieniem stron w kategoriach, które znajdują się we wskazanej czwórce. Przykładowo, kategorią, którą trzeba pobrać wraz z podkategoriami, jest kategoria Individuals. Trzeba uważać, by przy przechodzeniu do podkategorii odfiltrować te, z których strony były już pobierane, inaczej skończyłoby się to nieskończoną pętlą. Zdarza się bowiem tak, że jedna z podkategorii jest w więcej niż jednej kategorii wyższego poziomu.

Następnie wszystkie strony, które do tej pory były reprezentowane jako nagłówki, zostają pobrane wraz z całą treścią, kategoriami, i sparsowanymi szablonami. Wikia nie udostępnia sparsowanych szablonów, ponieważ jej wersja MediaWiki nie daje tej możliwości, dlatego trzeba pobraną z Wikii stronę przepuścić przez dodatkowy parser, np. przez ten publicznie dostępny na Wikipedii, chociaż ja używam postawionego lokalnie MediaWiki z powodów wydajnościowych. Zanim strony będą procesowane, następuje odfiltrowanie tych, które nie powinny się tu znaleźć. W tej grupie zawierają się strony zbiorcze i przeglądowe, które, chociaż znajdują się w pożądanych kategoriach, nie reprezentują postaci fikcyjnych, a, przykładowo, opisują jakąś ich grupę w egzotycznym kontekście, np. grupują postacie, które były tylko złudzeniami innych postaci, lub grupują reprezentantów jakiejś rasy, którzy nie byli znani z imienia.

Następnie dla większości encji, które mają być stworzone, poszukiwane są szablony znany z Wikipedii jako infoboksy – to te tabelki po prawej stronie na Wikipedii, z wypisanymi w nich najważniejszymi informacjami. Te szablony są, obok kategorii, w których znajduje się strona, głównymi źródłem danych w modelu. Infoboksy z Memory Alpha są w Star Trek API reprezentowane przez osobne obiekty względem encji bazodanowych, i w jeszcze kolejnych obiektach trzymany jest metamodel dla szablonów, czyli lista pól, które w nich występują, przykładowo, data urodzenia, płeć lub rasa, do której przynależy postać. Chociaż oddzielenie szablonów od encji może wydawać się nadmiarowe, posługiwanie się tą samą encją dla dwóch różnych zadań, wydawało mi się z kolei niezgodne z dobrymi praktykami. Z czasem okazało się też słuszne, bo czasami jednak procesor wypluwający z siebie encję reprezentującą szablon jest używany do czego innego, niż stworzenie na jego podstawie encji bazodanowej.

Procesory mogą być całkiem proste, i ograniczać się do jednej klasy bez zależności, jak w przypadku tego procesora, jak i całkiem skomplikowane, jak ten, który i tak tylko deleguje parsowanie kolejnych pól szablonu do swoich zależności. Dane zawarte w szablonach mogą i często są niekompletne, dlatego też model w Star Trek API ma niewiele obowiązkowych kolumn. Zwłaszcza w przypadku informacji ze świata fikcyjnego ciężko o komplet informacji – jeżeli postać epizodyczna nie miała podanej rasy, prawdopodobnie nigdy już nie będzie jej miała, chyba że wystąpi jeszcze w przyszłości w jakimś komiksie.

Zasadniczo w Star Trek API istnieją dwa typy procesorów. Jednym typem procesorów są te ze Spring Batcha o sygnaturze O process(I item) throws Exception, a drugi typ to – napisany już przeze mnie – ItemEnrichingProcessor o sygnaturze void enrich(I enrichablePair) throws Exception. Mój procesor do transportu obiektów używa EnrichablePair, czyli obiektu, który przechowuje zarówno wejściowy, jak i wyjściowy obiekt – a więc ten, który ma zostać wzbogacony. Ten drugi procesor jest używany wówczas, gdy zachodzi konieczność ustawienia więcej niż jednego pola w obiekcie reprezentującym szablon, lub zaczerpnięcia danych z więcej niż jednego pola. O bojach z danymi, które przechodzą przez procesory, będę pisał więcej w kolejnych tygodniach.

Spring Batch

Zaczynając pracę nad Star Trek API poszukiwałem narzędzia, które pozwoliłoby na elastyczną konfigurację wieloetapowego procesu zasilenia bazy danych danymi z wielu źródeł. Nie znałem takiego narzędzia z pierwszej ręki, ale chcąc uniknąć eksperymentów i konieczności wycofywania się z raz podjętej decyzji, postawiłem na najpopularniejsze dostępne rozwiązanie – Spring Batch, i jak dotąd, sprawdza się on bardzo dobrze, z wyjątkiem dwóch niedogodności, o których napiszę na koniec.

Spring Batch dostarcza ogólny szkielet, który wypełniamy naszą logiką biznesową. Na najwyższym poziomie w Spring Batchu znajdują się joby, których możemy mieć wiele i które mogą być wykonywane sekwencyjnie lub współbieżnie. W każdym jobie znajduje się wiele kroków, które również mogą być wykonywane sekwencyjnie lub współbieżnie. Kroki mogą być restartowalne, wykonywane raz, lub mieć limit wykonań. W moim przypadku pożądanym było, by każdy krok wykonywał się raz, ponieważ przyjąłem założenie, że baza będzie każdorazowo zasilana od nowa. Dopiero w przyszłości zaimplementuję zasilanie przyrostowe, które będzie ograniczone do tych danych źródłowych, które zmieniły się od ostatniego zasilenia.

Krok w Spring Batchu składa się z readera, procesora i writera. Reader, zależnie od implementacji, ładuje wszystkie dane przez rozpoczęciem kroku, lub doładowuje je przed wywołaniem procesora. Procesor implementuje interfejs ItemProcessor<I, O>, gdzie I to typ obiektu, od którego zaczyna się przetwarzanie, a O to typ zwracanego obiektu. Procesor ma tylko jedną metodę o sygtaturze O process(I item). Procesory mogą być springowymi beanami i dzięki temu logika wewnątrz pojedynczego procesora może być dowolnie rozbudowana. Ostatnim elementem kroku jest writer, który zapisuje wynikowe obiekty. Mogą być one zapisane gdziekolwiek, np. na dysk lub do jakiegoś cache’u, do użycia w jednym z kolejnych kroków, ale w przypadku startrekowego API naturalnym jest, by przetworzone encje trafiały do bazy danych.

Pojedynczy krok w Star Trek API najczęściej wiąże się z zasileniem pojedynczej tabelki w bazie danych. Aby stworzyć krok, najpierw trzeba wyznaczyć grupę kategorii, w których znajdują się strony mogące się przełożyć na wiersz w tabeli. Przykładowo, taką kategorią, od której może zacząć się krok, są Companies, czyli firmy, które pracowały nad Star Trekiem. Następnie do pamięci ładowane są nagłowki stron z danej kategorii. Nagłówki zawierają ID strony, jej nazwę, oraz źródło – Memory Alpha lub Memory Beta. Na tym kończy się reader. Następnie, już w procesorze, na podstawie takich nagłówków jest pobierana cała treść strony, i wóczas wykonywane jest przetworzenie w sposób właściwy dla danej encji, które kończy się przemapowaniem encji reprezentującej szablon z wiki na encje bazodanową. Szablony to infoboxy, które na pewno każdy widział na Wikipedii po prawej stronie wielu stron. Na koniec mamy zapis, które obejmuje dodatkowo odfiltrowanie duplikatów. Duplikaty biorą się z faktu, że na wiki istnieją przekierowania, co powoduje, że dwa różne nagłówki stron mogą wskazywać na tę samą stronę.

Kolejne kroki przetwarzające encje są konfigurowalne przez javowe propertisy. To już funkcjonalność, którą dopisałem sam. Każdy krok można uruchomić oddzielnie, lub można uruchomić kilka wybranych kroków, a inne wyłączyć. Kroki nie posiadają między sobą zależności i można je wykonywać w dowolnej kolejności i jako dowolny podzbiór. Zależności istnieją tylko między encjami bazodanowymi. Jeśli, przykładowo, chcę przetworzyć komiksy bez przetworzenia wpierw firm, wówczas wszystkie komiksy będą miały wydawcę ustawionego na null, ale poza tym zostaną z powodzeniem zapisane.

Żeby już nie duplikować kodu na blogu, przykład tego, jak wygląda reader, processor i writer można zobaczyć tutaj.

A teraz o dwóch niedogodnościach.

Jedną niedogodnością, jaką napotkałem, wynikającą być może z początkowej nieznajomości Spring Batcha, była konieczność dwukrotnej zmiany formatu konfiguracji. Najpierw napisałem konfigurację javową, ale po jakimś czasie została ona wymieniona na konfigurację XML-ową z powodu trudności w testowaniu Javy, oraz dlatego, że więszkość tutoriali, na które natrafiałem, używała notacji XML-owej. Trzy tygodnie później doszedłem do wniosku, że XML-owa konfiguracja nie daje się łatwo konfigurować za pomocą propertisów, i ponownie wymieniłem ją na javową, tym razem przykładając się do napisania pełnych testów.

Drugą niedogodnością, wynikającą być może z chęci stworzenia zbyt wysokopoziomowego interfejsu przez autorów Spring Batcha, jest niemożność łatwego dostania się do repozytoriów, które odpowiadają za operacje na jobach i stepach. Potrzebowałem tego, żeby nie ładować danych do już zakończonych kroków, bo czasami może to trwać kilka minut, a ładowanie danych odbywa się w momencie tworzenia springowych beanów, czyli efektywnie w trakcie startu aplikacji. Po dłuższym grzebaniu w kodzie doszedłem do rozwiązania, które na pewno nie spodobało by się autorom Spring Batcha. Można je obejrzeć tutaj. Sprowadza się ono do odpakowania klasy SimpleJobRepository ze springowego proxy, a następnie do wystawienia kilku jego prywatnych pól, wydobytych refleksją, jako beany. Brzydkie rozwiązanie, ale działa.

Udział w DSP’2017

Postanowiłem wziąć udział w konkursie Daj się poznać. Dało mi to pretekst do otworzenia tego bloga.

Do konkursu zgłosiłem się o z projektem Star Trek API, który rozwijam na GitHubie od października 2016 r.

Star Trek API to próba uporządkowania danych na temat uniwersum Star Treka i ujęcia ich w model relacyjny, z wykorzystaniem swobodnie dostępnych w internecie źródeł. W kolejnym kroku celem jest wystawienie tych danych jako otwarte API, dostępne online. Główna faza developmentu zaczęła się, zanim zaczął się konkurs, i potrwa jeszcze po jego zakończeniu.

Głównym źródłem danych dla API jest w tej chwili Memory Alpha, hostowane na Wikii wiki dotyczące kanonicznego uniwersum Star Treka, i – w znacznie mniejszym stopniu – Memory Beta, inne wiki dotyczące uniwersum Star Treka, które gromadzi informacje o fanowskich produkcjach.

Stos technologiczny w startrekowym API to Java i Spring do kodu produkcyjnego, oraz Groovy i Spock do testów. To stos, którego używam na co dzień w pracy, a chciałem, by projekt został napisany możliwie dobrze i bez eksperymentów z technologiami. To także stos na tyle popularny, że być może w przyszłości znajdą się chętni, by ten projekt ze mną rozwijać.

Inspiracją do pójścia w kierunku stworzenia API w uniwersum Star Treka były 2 projekty, na które natknąłem się w drugiej połowie 2016 roku w sieci: SWAPI oraz Pokéapi, czyli odpowiedni API dotyczące tematyki uniwersum Star Wars i API dotyczące Pokémonów. Star Trek był franczyzą, która zawsze mnie interesowała, i upewniwszy się, że żadnego podobnego projektu nie ma jeszcze w sieci, postanowiłem rozpocząć swój.

API ma docelowo zawierać zarówno dane z prawdziwego świata, dotyczące uniwersum Star Treka, czyli aktorów, seriale, firmy pracujące przy produkcji, komisy i książki, jak i szereg fikcyjnych bytów fikcyjnych: postacie, obiekty astronomiczne, rasy występujące w serialu, statki kosmiczne czy jedzenie. W tej chwili zaplanowanych jest ok. 35 encji, ale liczba ta może być ostatecznie zarówno niższa, np. przez połączenie kilku typów książek w jedną encję, jak i większa, w związku z rozszerzaniem o kolejne źródła danych.

W momencie pisania tego postu jestem w ok. 1/3 drogi do ukończenia wersji beta, którą będzie można bez obaw udostępnić online. Na stronie Work progress na wiki można śledzić postępy prac, mierzone ilością ukończonych encji.

Kolejne posty planuję publikować, przez czas trwania konkursu, zawsze w środy i w soboty.