Star Trek API jest już w sieci!

Star Trek API trafiło w końcu do sieci!

Zmiana priorytetów

Ostatni miesiąc poświęciłem w dużej mierze na doprowadzanie do działania strony głównej STAPI. Musiałem napisać dokumentację API, dorobić zliczanie statystyk encji i statystyk użycia endpointów, napisać trochę tekstów na stronę i pozamykać TODO, które oczekiwały na lepsze czasy. Tym niemniej dziś, w dniu końca konkursu, strona jest gotowa do pokazania światu.

Serwer

STAPI zostało postawione na serwerze dedykowanym Kimsufi. Tutaj moje doświadczenie było słabe. Na stronie Kimsufi.pl widnieje przechwałka, że serwer zostanie uruchomiony w 120 sekund. Uruchomienie mojego zabrało prawie 3 dni. Zamówiłem go w piątek po 17:00. 20 minut później wysłałem maila ponaglającego do supportu, który pozostał bez odpowiedzi aż do 14:30 w niedzielę. Wtedy dostałem odpowiedź, że w 120 sekund stawiane są serwery dla powracających klientów, a od nowych zazwyczaj wymaga się dokumentów potwierdzających tożsamość. U mnie obyło się bez wysyłania dokumentów, ale weryfikacja płatności trwała do 16:15 w poniedziałek. Przez cały poniedziałek wysłałem 4 kolejne maile ponaglające, które pozostały bez odpowiedzi.

System

System operacyjny w Kimsufi instaluje się przez webowy panel administracyjny. Na system wybrałem Ubuntu, które znam stosunkowo najlepiej. Na początku zainstalowałem wersję 17.04 (Zesty Zapus), ale ponieważ tutoriale na blogach, z których korzystałem, nie były z nią kompatybilne, cofnąłem się do wersji 16.04 (Xenial Xerus), która jest LTS-em.

Java i Oracle

Po tym, jak zainstalował się system, trzeba było zainstalować Javę 8. To robi się trzema łatwymi do wygooglania poleceniami, więc nie będę się nad tym rozwodził.

Grubszy problem wystąpił z bazą danych Oracle XI 11g, która nie jest oficjalnie wspierana na Ubuntu. Ponieważ jednak wśród wspieranych przez Oracle’a dystrybucji Linuxa nie było żadnej, którą można było zainstalować z panelu Kimsufi, postanowiłem pójść za najlepiej widocznym w Google tutorialem na temat instalacji Oracle’a na Ubuntu, jaki znalazłem. Jedyne, co musiałem zmienić w całej procedurze (napisanej dla Ubuntu 12.04), to sposób zaktualizowania parametrów jądra. Zamiast polecenia sudo service procps start, które nie chciało działać, wykonałem polecenie sysctl --system.

Deployment

Następnie trzeba było zasilić bazę z gotowego dumpu, który wygenerowałem kilka dni temu, oraz wrzucić wygenerowany plik WAR na Tomcata. Na tym etapie życia projektu proces deploymentu jest jeszcze mocno ręczny, ale w przyszłości na pewno go sobie zautomatyzuję. Na tę chwilę z automatu podmieniam tylko propertisy i generuje dumpa z innym schematem, niż schemat źródłowy, ale paczkę i bazę wgrywam z palca.

Problemy

Nic nigdy nie działa od pierwszego strzału, i tak samo było tym razem. Po wrzuceniu WAR-a na Tomcata nie było widać dokumentacji, ponieważ kontrakty nie były kopiowane do paczki. Ponadto okazało się, że część endpointów nie odpowiada, ponieważ ich nazwy były krótsze, niż sześć znaków, i leciał wyjątek, bo nie zabezpieczyłem się przed trywialnym StringIndexOutOfBoundsException.

Podsumowanie

Cieszę się, że udało mi się postawić stronę w ostatniej chwili. Dzięki temu lepiej widać, ile udało mi się zrobić przez ostatnie miesiące, i ile jeszcze pracy przede mną. Dodatkowo mam nadzieję, że sprawdzi mi się podejście release early, release often, i fandom Star Treka zainteresuje się tym projektem, zanim przyjdzie mi skończyć go w pojedynkę. Chociaż wcale się przed tym nie uchylam.

CodeNarc – linter do Grooviego

Gdy zaczynałem pracę nad Star Trek API, naturalnym wydało mi się zastosowanie jakiegoś lintera do Javy. Wybór padł na popularny Chekstyle. To żadne zaskoczenie, ale ponad połowa kodu, który w tej chwili znajduje się w repozytorium projektu, to kod testów napisanych w Spocku, czyli w efekcie w Groovym. Udało mi się znaleźć tylko jeden linter do Grooviego, CodeNarc, który opiszę w tym poście.

Instalacja

Jeśli budujemy projekt za pośrednictwem Gradle’a, jak w moim przypadku, instalacja ogranicza się do dodania do Gradle’a pluginu:

apply plugin: 'codenarc'

I skonfigurowania:

codenarc {
configFile = "$rootDir/codenarc.groovy" as File
toolVersion = '0.26.0'
}

Następnie trzeba stworzyć plik codenarc.groovy w katalogu głównym projektu, lub w dowolnym innym, jeśli wskażemy go w konfiguracji. Gdy to zrobimy, mamy dostępne dwie nowe komendy:

gradle codenarcMain
gradle codenarcTest

Pierwsza komenda sprawdza, czy produkcyjny kod w Groovim jest zgodny z aktywowanymi w CodeNarcu regułami, a druga robi to samo dla kodu testów. Ponieważ w projekcie nie mam żadnego kodu produkcyjnego w Groovym, przy walidacji kodu posługuję się tylko tą drugą komendą.

Dodatkowo teraz, gdy wywołujemy komendę gradle check, wykonywane są też walidacje pochodzące z CodeNarcu.

Konfiguracja

Cała konfiguracja Codenarcu odbywa się we wspomnianym już pliku codenarc.groovy. Alternatywnie można skorzystać z konfiguracji przez plik z propertisami, ale ja wybrałem plik Groovy.

Postanowiłem zacząć od wrzucenia pliku ze wszystkimi regułami domyślnie włączonymi, i po kolei rezygnować z tych, które uznałem za bezcelowe lub niemające znaczenia. Mimo wszystko nie zrezygnowałem z dużej liczby reguł, ponieważ kod testów jest z natury prostszy, niż kod produkcyjny, a więc nie tak wiele rzeczy trzeba było poprawiać, żeby walidacje przechodziły.

Ostatecznie mój plik z regułami wygląda tak.

Wyłączone reguły

Oto wybrane reguły, które wyłączyłem w Star Trek API, wraz z powodami wyłączenia:

  • NoTabCharacter – ponieważ w całym projekcie używam tabów.
  • AbstractClassWithoutAbstractMethod – ponieważ czasami moje klasy abstrakcyjne mają tylko pola, których wartości używane są w testach.
  • TrailingComma – w testach rzadko zmienia się kolejność w listach po ich utworzeniu.
  • ClassJavadoc – zgodnie z regułami clean code’u, piszę bardzo mało dokumentacji do kodu. Jeszcze mniej sensu miałoby pisanie jej w testach, gdzie, IMO, dostateczną dokumentację stanowią nazwy metod testowych pisane językiem naturalnym.
  • SpaceAroundMapEntryColon – wydawało mi się nienaturalne, by konstruktor mapy zapisywać jako [ : ], a nie [:]. CodeNarc jest pierwszym miejscem, gdzie spotkałem się z takim zapisem.
  • MethodSize – domyślnie metoda może mieć sto linii. W przypadku metod testowych nie ma sensu robić wygibasów, by je podzielić, zwłaszcza że są już podzielone przez bloki given, when: i then:.
  • AUnnecessaryObjectReferences – reguła zabrania odwoływania się do jednego obiektu więcej, niż domyślne 5 razy. Często natomiast w testach mam sytuację, gdy robię assercje nawet na kilkudziesięciu polach jednego obiektu. Jako alternatywę CodeNarc podaje zapis with. Ten zapis nie jest niestety wspierany przez moje IDE, więc automatyczny refaktor go nie obejmie, i nie można korzystać w bloku with z podpowiadania składni.

Przekonfigurowane reguły

Następujących reguł nie wyłączyłem, ale je przekonfigurowałem:

  • LineLength – dopuszczalna długość linii została ustawiona na 150 znaków, podobnie jak w Checkstyle’u.
  • BuilderMethodWithSideEffects – CodeNarc traktował wszystkie metody, które zaczynały się od create jako metody wytwórcze, które nie mogą mieć efektów ubocznych. Ja natomiast mam metody testowe, których opis w języku naturalnym zaczyna się od create, więc zmieniłem regułę tak, aby były brane pod uwagę tylko te metody, które zaczynają się od create, po którym następuje wielka litera.
  • PackageName – musiałem dopuścić camel case w nazwach paczek.
  • Najczęściej ignorowane reguły

    Oto lista reguł, które najczęściej ignoruję w pojedynczych metodach lub klasach:

    • LineLength – w niektórych miejscach lepiej nie łamać linii, gdy są za długie, bo czytelność jest gorsza. Używam też tego wykluczenia w przypadku testów z datatables.
    • RuntimeException – w niektórych testach asercje wykonuję w pętlach, więc rzucam z nich wyjątki. Nie widziałem potrzeby tworzenia nowego typu wyjątku tylko na ten przypadek, więc dozwoliłem rzucanie wyjątków typu RuntimeException.
    • BracesForMethod – czasami metoda ma długi opis, i wtedy jest zapisywana z trzema cudzysłowami, a nie z jednym. W tym przypadku trzeba wyłączyć regułę BraceForMethod.

    Czego można się dowiedzieć

    Podczas implementowania CodeNarcu dowiedzieć się można kilku ciekawy rzeczy. Ja dowiedziałem się następujących:

    • Ostatnie domknięcie w ciągu metod może być zapisane bez nawiasów okrągłych. Czyli .do { it.stuff() } zamiast .do({ it.stuff() }).
    • Nie powinno się zapisywać metod voidowych jako def something(), ponieważ def oznacza tyle, co Object, a metody voidowe, takie jak metody testowe, niczego nie zwracają.
    • Nie ma sensu zapisywać stringów w podwójnych cudzysłowach, jeśli nie robimy w nich interpolacji. Także z powodów wydajnościowych.

    Podsumowanie

    Zastanawiam się, ile osób używa CodeNarcu w stosunku do produkcyjnego kodu w Groovim. Na GitHubie ma on zaledwie 120 gwiazdek, w porównaniu do 1985 gwiazdek, które ma Checkstyle.

    CodeNarc na pewno poprawia jakość kodu, który piszemy. Pewnie miałbym z niego więcej użytku, gdybym w Groovim nie pisał tylko testów. Mimo wszystko pomaga, choćby w usuwaniu nieużywanych zmiennych i formatowaniu kodu.

    EhCache i Hibernate

    Planuję doprowadzić development Star Trek API do końca. Będę przez jakiś czas, być może całkiem długi czas, jedyną osobą, która będzie płaciła za utrzymanie serwera, na którym API będzie działało. Nie planuję inwestować w taki serwer zbyt wielu środków, dlatego w moim interesie jest, by zrealizować wydajne cache’owanie. Przyda mi się ono na pewno, biorąc pod uwagę fakt, że do tej pory do Pokéapi zostało wykonane 175 milionów zapytań.

    Wymagania

    Wymagania dotyczące cache’u w Star Trek API są relatywnie proste, w porównaniu do innych systemów. Dane, które trafią do bazy, czeka jeden z dwóch scenariuszy. W pierwszym scenariuszu dane nie będą się zmieniać, gdy już trafią do bazy, ponieważ zasilenie dokona się poza serwerem aplikacyjnym, i serwerowi aplikacyjnemu zostanie tylko wskazana napełniona baza. Drugi scenariusz jest taki, że dane zmienią się co prawda, ale będę mógł kontrolować moment, gdy się to stanie, i wówczas będzie można wyczyścić cache. Problem inwalidacji cache’y w zasadzie nie będzie istniał.

    Instalacja i konfiguracja

    A móc używać EhCache, po pierwsze trzeba dodać do projektu zależności. Ja dodałem dwie:

    compile group: 'net.sf.ehcache', name: 'ehcache'
    compile group: 'org.hibernate', name: 'hibernate-ehcache'

    Po drugie, trzeba skonfigurować przynajmniej domyślny cache. EhCache pozwala na składowanie cache’owanych zasobów w wielu regionach, ale zdecydowałem na początek, że przynajmniej póki nie będę wiedział więcej o tym, jakie zasoby są najczęściej kwerendowane, nie chcę przekombinować z konfiguracją. Moja obecna konfiguracja wygląda więc tak, i jest najbardziej podstawową wersją, jaka może być.

    Integracja z Hibernatem

    EntityManagerFactory musi dostać kilka dodatkowy propertisów, widocznych tutaj. Trzeba więc między innymi włączyć cache drugiego poziomu, a także wskazać EhCache jako fabrykę regionów cache’u. Oznacza to klasę implementującą interfejs zdefiniowany przez Hibernate’a, który gwarantuje wytworzenie kilku kolejnych obiektów, odpowiedzialnych kolejno za cache’owanie między innymi kolekcji, wyników zapytań, i encji.

    Strategia cache’owania

    Wymyśliłem, że dodatkowo będę miał własną strategię cache’owania. Interfejs jest prosty i wygląda tak. Strategia dostanie QueryBuilder, czyli obiekt, w którym przed wykonaniem zapytania gromadzone są jego parametry (kryteria zawężające, sortowanie, relacje do pobrania). Strategię cache’owania można zdefiniować w propertisach aplikacji. Napisałem trzy strategie. Jedną, która zawsze zwraca prawdę, druga, która zawsze zwraca fałsz, i trzecią, najciekawszą, której chce domyślnie używać, i która zwraca prawdę warunkowo, gdy jedynym kryterium w zapytaniu jest UID, czyli unikalny identyfikator obiektu.

    Cache’owanie encji

    Gdy już mamy wszystko zintegrowane, trzeba wyznaczyć encje, które będą cache’owane. W moich przypadku są to wszystkie encje, które trafiają do REST-owego i SOAP-owego API. Do encji trzeba było dodać adnotację @Cache, tak jak zostało zrobione tutaj. Dodatkowo napisałem test, który z pomocą refleksji pilnuje, żeby wszystkie encje, poza jawnie określonymi, miały zawsze adnotację @Cache. Napisałem też drugi test, który to samo robi dla pól zawierających relacje jeden do wielu i wiele do wielu.

    Testy

    Pozostało mi przetestować ten setup. Testy manualne polegały na zapytaniu kilku endpointów i obserwowaniu, jakie zapytania odkładają się w logach za pierwszym i za kolejnymi razami. Za kolejnymi razami było tych zapytań zdecydowanie mniej, chociaż ciągle jeszcze jakieś się pojawiają i będę musiał wrócić do tematu, żeby maksymalnie wyżyłować EhCache. Prędkość odpowiedzi endpointów za kolejnymi zapytaniami znacząco wzrosła.

    Podsumowanie

    Przewiduję, ze klienty będą wykonywać relatywnie mało zapytań przeszukujących bazę, w których zestaw kryteriów zapytania jest słabo powtarzalny, a relatywnie dużo zapytań, które pytają o konkretny zasób za pośrednictwem UID. To założenie może z łatwością okazać się nieprawdziwe, a wtedy trzeba będzie przestrajać cache. Ale to może być temat na kolejny post, za kilka miesięcy.

    Różnice między modelem z uniwersum Star Treka i modelem z prawdziwego świata

    W trakcie pracy nad Star Trek API spotykam dwa rodzaje modeli. Jedne to modele dotyczące uniwersum Star Treka, a drugie to modele dotyczące prawdziwego świata. Do modeli dotyczących uniwersum zaliczyć można lokalizacje, obiekty astronomiczne, jedzenie czy statki kosmiczne, a do modeli z prawdziwego świata – filmy, aktorów, komiksy czy firmy pracujące nad produkcją. Istnieją między nimi różnice, które opiszę w tym poście.

    Ilość

    Pierwsza różnica to różnica w ilości modeli. Tych z prawdziwego świata jest trochę mniej. Obecnie mam ich zrealizowanych 13, a zaplanowanych kolejne 6. W przypadku modeli z uniwersum zrealizowanych jest 6, a zaplanowanych do realizacji – kolejne 17. Postęp moich prac można śledzić tutaj. Także ilość krotek w modelach różni się. Osób pracujących nad Star Trekiem jest ponad 6000, a aktorów, kaskaderów i innych wykonawców widocznych na ekranie – ponad 5000. Jeśli chodzi o modele z uniwersum, najwięcej, około 5000, jest postaci, a potem są już obiekty astronomiczne i miejsca, jedne i drugie z wynikiem poniżej 2000 krotek.

    Relacje

    Modele z prawdziwego świata mają znacznie więcej relacji, niż modele z uniwersum, i są one znacznie lepiej uporządkowane. Przykładowo, ksiązki mają aż 11 relacji wiele do wielu: serie książek, autorów, artystów, edytorów, narratorów audiobooków, wydawców, wydawców audiobooków, postacie, identyfikatory wydań książek (ISBN, ASIN) i identyfikatory wydań audiobooków, oraz kolekcje książek. Modele z uniwersum, które mają najwięcej relacji, to obiekt astronomiczny, seria książek i seria komiksów. Mają one relacje do nadrzędnego elementu i do podrzędnych elementów tego samego typu. Większość modeli z uniwersum nie ma żadnych relacji.

    Modele z uniwersum mają jednak na ogół więcej flag booleanowych. Każda kategoria, np. dotyczące lokalizacji, może być przetłumaczona na jedną flagę w modelu. I tak w encji Localization znajdujemy takie flagi, jak bodyOfWater, colony czy school. Większość flag dla większości krotek będzie naturalnie ustawiona na fałsz.

    Kompletność

    Model z prawdziwego świata ma zazwyczaj więcej danych, niż model z uniwersum, z oczywistego powodu: wydarzenia muszą mieć daty, ludzie nazwiska, a książki zawsze określoną ilość stron. W przypadku modeli z uniwersum wiemy tylko tyle, ile powiedziano w kanonicznych produkcjach. Najczęściej jest tak, że jakiś byt dający się ująć w model występuje w serialu przez chwilę, jest wspomniany raz, i nigdy więcej nie zostaje potem użyty. Przykładem niech będzie ten kawior, o którym wiemy tylko tyle, że był słony, ale nawet ta informacja jest zbyt egzotyczna, by ująć ją w model.

    Model z prawdziwego świata można próbować uzupełniać w innych źródłach. Można spróbować zgadnąć płeć aktora na podstawie płci jego imienia lub znaleźć jego datę urodzenia w IMDb. W przypadku modelu z uniwersum, który nie ma kompletnych danych, można tylko liczyć, że dany byt zostanie jeszcze w przyszłości wspomniany w jakimś medium.

    Podsumowanie

    Mogąc wybierać między modelami z prawdziwego świata i tymi z uniwersum, wolę pisać kod parsujący te drugie. Jest to łatwiejsze, szybsze i odkrywam wówczas model, który jakoś nakłada się na moje wspomnienia z oglądania kolejnych sezonów Star Treka. Ale żeby API było kompletne, nic nie może zostać potraktowane po macoszemu. Z drugiej strony, przez ostatnie miesiące częściej brałem się za modele z prawdziwego świata, niż za modele z uniwersum. Robiłem tak dlatego, żeby nie stracić zainteresowania projektem i najlepsze zostawić sobie na koniec.

    Stałe ID obiektu w API przy zmieniającym się ID w bazie danych

    Podczas tworzenia Star Trek API szybko doszedłem do wniosku, że nie mogę ujawniać klientom API ID-ków bazodanowych. Powodem było to, że ID-ki te nieustannie się zmieniały. Ilekroć robiłem zasilenie od nowa, było niemal pewne, że dostanę nieco inne zestaw stron, które zostaną przetworzone w innej, niż poprzednio, kolejności. Wydało mi się oczywiste, że gdybym sam chciał skorzystać z danych tego typu, jak udostępniane przez moje API, oczekiwałbym, że ID będą stałe. Chciałbym je gdzieś zapisywać i posługiwać się nimi w przyszłej komunikacji z API.

    Zadanie

    Trzeba było wymyślić sposób na to, aby generować stałe, lub przynajmniej w ogromnej większości przypadków stałe identyfikatory dla wszystkich encji. Dodatkowym bonusem byłoby to, aby identyfikatory były unikalne w obrębie całego API.

    Wykonanie

    Memory Alpha i Memory Beta mają własną numerację stron, którą na szczęście ujawniają. Każda strona ma ID, który nie zmienia się przez cały cykl jej życia, nawet wówczas, gdy strona zostanie przeniesiona pod inną nazwę. Identyfikatory stron z z wiki mogły więc zostać pierwszą ważną częścią identyfikatora, który już wówczas zacząłem nazywać GUID-em, a któremu ostatnio zmieniłem nazwę na UID, by nie było wątpliwości, że to co innego, niż GUID.

    Od razu było jasne, że potrzebuję jeszcze czegoś, co identyfikowałoby encję w unikalny sposób. ID-ki z Memory Alpha i Memory Beta mogły się powtarzać. Do UID-ów dołączyłem więc 2 znaki, które reprezentowały encję, oraz 2 znaki, które reprezentowały źródło. W ten sposób powstał na przykład UID, który wyglądał tak: CLMA0000108985. Dwa pierwsze znaki, CL, oznaczały encję ComicCollection, dwa kolejne, MA, oznaczały, że źródłem jest Memory Alpha. Natomiast 10 cyfr to ID z wiki. Razem zawsze 14 znaków, co wydało mi się wystarczające na API tej wielkości.

    Pierwsze, i na razie jedyne, kłopoty

    Pierwszy i na razie jedyny problem, który napotkałem, dotyczył bytów, które nie mapowały się na dokładnie jedną stronę z wiki. Chodziło o encje reprezentujące numery ISBN i ASIN. Na jednej stronie mogło występować kilka takich numerów.

    Na szczęście numery ISBN mają 10 lub 13 znaków, a numery ASIN – 10 znaków. Łatwo było z nich wygenerować 14-znakowe UID-y. 10-znakowym numer ISBN mają po prostu format ISBN160010603X, 13-znakowe ISBN-y format I9781566199094, a 10-znakowe numery ASIN-y – format ASINB008L4YH4W. Można więc w dalszym ciągu zagwarantować unikalność UID-u w systemie.

    Implementacja

    Ostatecznie wszystkie te wymagania zostały zaimplementowany w klasie UidGenerator. Klasa jest wołana zawsze, zanim encja zostanie pierwszy raz zapisana, i w ten sposób UID zawsze jest obecny.

    UID-y zastępują w API bazodanowe identyfikatory. To nimi pyta się o pojedyncze REST-owe i SOAP-owe obiekty. UID-y też, wraz z nazwami lub tytułami, trafiają do encji nagłówkowych, takich jak MovieHeader. Na ich podstawie, gdy ktoś będzie chciał, będzie mógł pobrać więcej danych na temat interesującej encji.

    UidGenerator podczas inicjowania aplikacji sprawdza, czy będzie potrafił wygenerować unikalne identyfikatory dla wszystkich encji. Jeśli nie, trzeba dodatkowo dodać identyfikator skrócony identyfikator encji w statycznej mapie. W nieprawdopodobnym przypadku, gdy ID strony z Memory Alpha lub Memory Beta będzie większy, niż 9999999999, zostanie rzucony wyjątek, którym będzie trzeba się zająć. Ale to temat na odległą przyszłość.

    Throttling API w Springu

    Publicznie dostępne API, takie jak Star Trek API, w miarę, jak zyskują popularność, muszą poradzić sobie ze zwiększającym się ruchem. Jedną ze strategii radzenia sobie ze zbyt wieloma zapytaniami jest nałożenie limitu na klienty. Można to zrobić na kilka sposobów, np. implementując limit godzinny lub dobowy dla danego adresu IP, lub odnawialne, płatne lub odświeżane cyklicznie, limity dla kluczy API.

    Istniejące implementacje

    Spodziewałem się, że ten problem został już rozwiązany i wystarczy dorzucić do projektu zależność, która zajęłaby się throttlingiem. Tak jednak nie jest. Istnieje co prawda Apache CXF Throttling Feature, ale analizując kod doszedłem do wniosku, że nie nadaje się on do throttlingu w sytuacji, gdy mamy wiele klientów. Nadaje się jedynie do throttlingu per zasób, a to mnie nie zadowala. W internecie można znaleźć też posty polecające RateLimiter z Guavy, ale tutaj z kolei filozofia jest taka, że limitujemy dostęp do zasobu, a wątek, który chce zająć zasób, ma czekać, jeśli zasób jest już zajęty. To nie wpisuje się w moją wizję.

    Założenia

    Postanowiłem zaimplementować omawianą funkcjonalność samodzielnie. Na pierwszy ogień poszedł throttling na podstawie adresu IP. Założenia były proste. Adresy IP, które uzyskują dostęp do API, będą zapisywane w bazie. Pierwszy zapis adresu IP ustawi jego maksymalny limit zapytań na 100, a ilość pozostałych zapytań na 99. O każdej pełnej godzinie limity są odświeżane i każdy adres IP może znów wykonać 100 zapytań. Co godzinę, 30 minut po pełnej godzinie, adresy które nie wykonywały zapytań dłużej, niż dzień, są usuwane.

    Model

    Postanowiłem nie zamykać się na możliwość, że w przyszłości zaimplementuję także throttling na podstawie kluczy API, a więc schemat bazy danych przygotowałem pod obie te ewentualności. W ten sposób powstał model oraz towarzyszące mu repozytorium z niezbędnymi operacjami.

    Interceptory

    Skoro było już skąd pobrać i gdzie zapisać informację o tym, czy dany adres IP może dalej wysyłać zapytania do API i otrzymywać poprawne odpowiedzi, trzeba było zaimplementować interceptory, które pozwoliłyby odpowiednio wcześnie zdecydować o tym, że przetrwarzania żądania powinno zostać anulowane. Nie chodzi wszak o to, żeby zapytać bazę, a na koniec zamiast poprawnej odpowiedzi pokazać komunikat o błędzie.

    Na początek trzeba było stworzyć interfejs ApiThrottlingInterceptor. Bean implementujący interfejs zostaje zarejestrowany w trakcie tworzenia CXF-owego serwera, oraz we wszysktich endpointach SOAP-owych, w ich wspólnej metodzie wytwórczej. Interfejs będzie implementowany przez dwie klasy, zależnie od tego, czy jest włączony springowy profil odpowiedzialny za throttling, czy nie. Wówczas, gdy profil nie jest obecny, jako bean zostanie zarejestrowana klasa, która nic nie robi. Jeśli profil jest obecny, zostanie zarejestrowana klasa, która deleguje nadchodzące zapytanie do fasady.

    Proces walidacji

    Zadaniem fasady jest przekazanie nadchodzącego zapytania do walidatora i, jeśli walidator nie pozwoli, by zapytanie było dalej procesowane, rzucenie wyjątku, inego dla REST-owych, a innego dla SOAP-owych zapytań.

    Walidator ma na celu pobranie adresu IP z zapytania i sprawdzenie, czy ten adres IP może jeszcze wykonywać zapytania. Jeśli tak, zwraca DTO z flagą throttle ustawioną na fałsz, i pustymi komunikatem o błędzie. Jeśli adres IP nie może już wykonywać zapytać, zwracany jest DTO z flagą throttle ustawioną na prawdę, oraz powodem throttlingu.

    Komunikaty o błędach

    Jeśli chodzi o SOAP-owy wyjątek, rzucenei SoapFault-a wystarczy, by CXF zamienił go na komunikat w spodziewanym formacie. W przypadku REST-owego błędu trzeba była napisać customowy mapper, który przemapowuje REST-owy wyjątek na format zdefiniowany w swaggerowym kontrakcie.

    Procesy cykliczne

    Na koniec trzeba było stworzyć dwa procesy cykliczne. Jeden do odświeżania limitów IP, a drugi do kasowania starych wpisów z adresami IP. Oba jako jedyną zależność mają repozytium encji Throttle, i po prostu wykonują na nim wymagane operacje.

    Podsumowanie

    I to właściwie wszystko. Jedyną podstępną rzeczą, na którą trzeba uważać w implementacji takiej jak ta, to by dekrementować ilość pozostałych zapytań za pomocą SQL, a nie Javy. Może się bowiem zdarzyć, że pobierzemy dla kogoś limit i zechcemy go w następnej operacji zapisać pomniejszony o 1, a w międzyczasie proces cykliczny ponownie ustawi limit na 100. Czyli trzeba zrobić tak.

    Myślę, że zaimplementuję w przyszłości także throttling na podstawie kluczy API. Nie robię tego w tej chwili, bo w przeciwieństwie do limitowania dostępu do API na podstawie adresu IP, limitowanie na podstawie kluczy wymaga, by powstał jakoś frontend, do którego można się zalogować, najlepiej GitHubem, Facebookiem i Googlem. Wymagane byłoby tu też, aby każdy zalogowany użytkownik mógł swoimi kluczami zarządzać, czyli przynajmniej tworzyć nowe, inwalidować stare, a być może też requestować większe limity lub je dokupować. To praca, której nie chcę na razie wykonywać, i wolę skupić się na skończeniu samego API.

    Brakujący feature JPA

    Ten post będzie jedynie narzekaniem na niedoskonałość technologii, z którą pracuję nad projektem zgłoszonym do DSP.

    Problem

    W trakcie tworzenia Star Trek API muszę modelować relacje wiele do wielu w znacznej ilości. Często jest tak, że dwie encje mają między sobą kilka relacji wiele do wielu. Relacje te oznaczają oczywiście co innego. Np. mam filmy, a w nich relacje do ludzi pracujących nad Star Trekiem w różnych rolach. Oddzielnie są producenci, oddzielnie autorzy scenariusz, i tak dalej. W kodzie wygląda to na przykład tak:

    Jak widać, relacja między encją Movie i encją Staff występuje więcej niż raz. Trzeba więc utworzyć dwie tabelki pośrednie, movies_screenplay_authors i movies_story_authors, dla obu tych relacji. Jeśli tabelek jest więcej, to trzeba utworzyć więcej relacji. W efekcie w w bazie mam tabelkę dla filmów i relacje filmów z innymi encjami:

    movie
    movies_characters
    movies_directors
    movies_performers
    movies_producers
    movies_screenplay_authors
    movies_staff
    movies_stand_in_performers
    movies_story_authors
    movies_stunt_performers
    movies_writers

    Z czasem coraz ciężej przeglądać mi bazę w SQL Developerze. Tabelek z danymi mam w tej chwili niespełna 20, a tabelek z relacjami ponad 50.

    Rozwiązanie, które nie istnieje

    To, czego brakuje w obecnej sytuacji, to możliwość użycia jednej tabelki pośredniej dla kilku relacji. Wydaje mi się, że jest to niemożliwe w JPA 2.1. Wyobrażam sobie, że od strony kodu wyglądać mogłoby to tak:

    W ten sposób nasza implementacja JPA, np. Hibernate, wiedziałaby, że aby utworzyć relację między filmem i scenarzystą, musi dodać wpis do tabelki movies_staff, dodatkowo ustawiając wartość kolumny staff_type na "STORY_AUTHOR". Abstrahuję tutaj od potencjalnych problemów wydajnościowych. Star Trek API, z maksymalnie kilkoma tysiącami encji w każdej z tabelek, z pewnością nie cierpiałoby z powodu problemów z wydajnością. Od strony SQL-a takie rozwiązanie również nie wydaje mi się niemożliwe.

    Any help?

    Byłoby wdzięczny za sugestie, czy da się opisywany problem jakoś rozwiązać. Może istnieją jakieś przeciwskazania do implentacji tego rodzaju funkcjonalności? A może istnieje ona w Hibernate, ale nie w JPA?

    Wczoraj zadałem podobne pytanie na StackOverflow. Okazało się, że to, o czym piszę, rzeczywiście nie jst możliwe w JPA, ale jest możliwe do osiągnięcia z użyciem DataNucleus, frameworka, który również implementuje JPA.

    Czy zmienię implementację JPA dla jednej funkcjonalności? Pewnie nie, ale dobrze mieć opcje.

    Zasada Pareta i pozyskiwanie danych

    Pozyskiwanie danych z Memory Alpha do Star Trek API to zarówno próba odkrycia wszystkich reguł i konwencji, które przyjęli ludzie, którzy przez lata tworzyli to wiki, jak i konieczność zmierzenia się ze wszystkimi niekonsekwencjami, które przy tworzeniu tak dużego wiki musiały się pojawić, oraz z wszystkimi tymi przypadkami, gdzie konwencje nie zostały wypracowane.

    Odkrywanie danych

    Odkrywanie danych i wielości formatów, w których są prezentowane, odbywa się stopniowo i przynajmniej początkowo nie wiąże się z pisaniem kodu. Najpierw po prostu przeglądam kilkadziesiąt lub sto kilkadziesiąt stron z kategorii źródłowych, ze szczególnym zwróceniem uwagi na zawartość szablonów i na kategorie, w których jest strona, żeby zyskać ogólne pojęcie o jakości i kompletności danych. Po tym wiem, które pola w szablonach są używane i jak często. Zdarza się bowiem, że pewne pola w szablonach, chociaż obecne, są wykorzystywane na przykład w 1% przypadków. Wówczas trzeba podjąć decyzję, czy w ogóle uwzględniać taką informację w modelu.

    Gdy mam już z grubsza opracowaną listę pól w szablonie, z których będą pozyskiwane dane, tworzonę często zbiorczy procesor, podobny do ComicsTemplatePartsEnrichingProcessor. Taki procesor deleguje parsowanie zawartości poszczególnych pól szablonu do swoich zależności.

    Standard i jego brak

    Tym, co jako pierwsze zazwyczaj implementuję, jest jakiś szeroko rozpowszechniony na Memory Alpha standard zapisu danej informacji, np. zapis daty gwiezdnej w formie 4506.4 – 4512.2. To pokrywa większość przypadków. Następnie, w tym samym procesorze, po prostu loguję wszystkie kolejne niepuste wartości, których nie udało się sparsować pierwszym sposobem, i szukam kolejnych reguł, które można by zaimplementować.

    Tutaj najlepiej widać zasadę Pareta. Najczęstszy format jest zarówno łatwo zrozumiały dla ludzi, jak i łatwy w implementacji, a w miarę, jak odkrywam kolejne zapisy tego samego typu informacji, są one zarówno mniej intuicyjne, jak i nieco trudniejsze w implementacji.

    W końcu redukuję listę możliwych formatów danej informacji do grupy niepowtarzalnych. Są to zarówno zanieczyszczone informacje, których parsery byłoby czasochłonne w pisaniu i niemożliwe do ponownego użycia, jak i niepoprawnie zapisane informacje, które należałoby poprawić na Memory Alpha. Zdarzają się też informacje, których nie można dopasować do modelu, chociaż są łatwo zrozumiałe dla ludzi.

    Po tym wszystkim pozostaje zaimplementowanie pozostałych, dających się dopasować do modelu danych, jako FixedValueProvider, np. taki jak ComicSeriesTemplateNumberOfIssuesFixedValueProvider.

    Nie ma sensu pisać kodu

    Dla pewnych typów danych nie ma sensu pisać procesora mierzącego się z treścią wiki. Przykładem jest tu SeriesEpisodeStatisticsFixedValueProvider, klasa zawierająca zapisane na sztywno statystyki seriali dotyczące ilości sezonów, ilości odcinków i ilości podwójnych odcinków. Stwierdziłem, że dla siedmiu serialu nie ma w ogóle sensu pisać kodu w Javie, i szybciej dostarczę te wszystkie statystyki w ten sposób. To skąpe i rzadko zmieniające się informacje. Myślę, że napisanie rusztowania dla procesora, który wyciągałby te statystyki bez zapisywania na sztywno, zajęłoby mi pewnie 10 lub 15 minut, czyli tyle, ile zajęło mi zapisanie tego na sztywno.

    Podsumowanie

    Odkrywanie danych w zmieniającym się źródle danych nigdy nie jest skończone. Mogą bowiem pojawiać się nowe strony, a stare mogą zmieniać zawartość. Dlatego ważne jest, żeby wszystkie te przypadki, gdy danych nie udało się sparsować, logować do konsoli w celu dalszej inspekcji i ewentualnej poprawy w przyszłości.

    Gdy heurystyka wie lepiej

    Jedną z danych ekstrahowanych z Memory Alpha jest płeć ludzi pracujących przy Star Treku, zarówno tych występujących jako aktorzy, statyści i kaskaderowie, jak i tych, których nie widać na ekranie. Proces określania płci składa się z kilku podkroków i zamknięty jest w tym procesorze.

    Metody nieheurystyczne

    Pierwsze, co robi ten procesor, to sprawdza, czy płeć dla danej osoby nie została zapisana na sztywno w słowniku. Słownik powstał, bowiem po tym, gdy wszystkie inne procesory zostały już napisane i przetestowane w boju, ciągle pozostawała mi jakaś lista stron na Memory Alpha, z których nie udawało się wyciągnąć płci. Płeć czasami można było wyczytać z opisu, ale zdarzało się też, że trzeba było ją określać na podstawie zdjęcia, lub na podstawie innych źródeł w internecie. Zdarzało się też, że informacje były na tyle skąpe, że na ich podstawie nie dawało się nic ustalić. Wówczas należało po prostu ustawić płeć na null i nie parsować strony dalej.

    Podejmowana jest także próba znalezienia płci na podstawie płci postaci, którą aktor grał. To nie sprawdza się w przypadku personelu pracującego nad produkcją Star Treka, chyba że osoby takie były także aktorami lub statystami.

    Jeśli zawiodą wszystkie inne metody, podejmowana jest próba zapytania zewnętrznego API o płeć imienia. Używam tutaj serwisu Genderize.io, który pozwala wykonać 1000 darmowych zapytań dziennie. Pełne zasilenie Star Trek API to od 150 do 200 zapytań do Genderize.io, więc limit jest więcej niż wystarczający. Genderize.io wraz z płcią imienia zwraca także prawdopodobieństwo, że imię przynależy do danej płci. Uznałem, że prawdopodobieństwo na poziomie 95% jest wystarczające, żeby zaakceptować wynik i nie parsować strony dalej. Jeśli także zapytanie do Genderize.io się nie powiedzieć, lub prawdopodobieństwo określenia płci na podstawie imienia będzie zbyt niskie, cały procesor odpowiedzialny za znalezienie płci zwraca null.

    Heurystyka

    Zanim zostanie podjęta próba znaleziania płci w zewnętrznym API, dokonywana jest analiza na podstawie zaimków i innych rzeczowników znalezionych na początku strony. Heurystyka określająca płeć na podstawie zaimków i innych rzeczowników jest prymitywna. Najpierw pobierane są 3 pierwsze paragrafy strony, a następnie wyszukiwane są słowa takie jak actor, himself, stuntman dla płci męskiej oraz actress, herself, stuntwoman dla płci żeńskiej. Ilość wystąpień jest podsumowywana, i jeśli wystąpień słów wskazujących na płeć męską jest więcej, niż wystąpień wskazujących na płeć żeńską, wtedy zwracana jest płeć męska. Analogicznie dla płci żeńskiej. Dodatkowo, jeśli wystąpień słów z jednej grupy nie jest przynajmniej 2 razy więcej, niż wystąpień słów z drugiej grupy, fakt ten jest logowany.

    Mimo doskonalenia w kolejnych iteracjach heurystyki do określania płci, zauważyłem, że jeden aktor, Michael Eugene Fairman, człowiek o wybitnie męskim imieniu, konsekwentnie był prezentowany w logach jako ten, który ma płeć żeńską. Wszedłem więc do artykułu, który go opisywał. Tam, na zdjęciu, dalej wyglądał na płeć męską. Dopiero wczytanie się w treść ujawniło, że:

    Since 21 January 2010, Fairman is legally known as Marie Michael Fairman following her transgender idenfication.

    I tak, ku mojemu zaskoczeniu, heurystyka zadziałała, bowiem reszta artykułu opisywała aktora w formie żeńskiej.

    SOAP vs REST (Swagger)

    Zaczynając pracę nad Star Trek API postanowiłem, że będzie można się z nim komunikować zarówno przez protokół SOAP, jak i za pomocą REST-u. O ile w przypadku SOAP-u wybór implementacji w Javie było oczywisty – wsdl2java opakowany w gradle’owy plugin, o tyle w przypadku REST-u nie było to już tak jednoznaczne. Nie ma zresztą jednego standardu do opisu REST-owych encji i serwisów. Ostatecznie zdecydowałem się na najpopularniejszą implementację, jaka istnieje: Swaggera, ale trzeba tutaj oddać, że RAML wydaje się równie dojrzały, co Swagger.

    W tym poście opiszę plusy i minusy mojego podejścia, oraz różnice między SOAP-em i REST-em z użyciem Swaggera.

    Oczywistym i od razu rzucającym się w oczy minusem tworzenia dwóch wersji modelu API jest konieczność utrzymywania dwóch wersji kodu opisującego encje i serwisy. Jeden kod w XML-u dla SOAP-u, a drugi kod w YAML-u dla REST-u. Ma to jednak plusy, ponieważ wymusza oddzielenie modelu bazodanowego od modelu API warstwą pośrednią, która musi zapewniać REST-owym i SOAP-owym endpointom jednolity model komunikacji i zapobiegnie przeciekaniu wygenerowanego kodu do warstwy repozytoriów. U mnie tę warstwę pośrednią stanowi kilka serwisów plus DTO reprezentujący kryteria wyszukiwania. Oddzielnymi mapperami, zarówno SOAP-owymi, jak i REST-owymi, zapytania są tłumaczone na wspomniany wspólny DTO, który służy do odpytania generycznego repozytorium. Następnie tymi samymi mapperami model bazodanowy jest tłumaczony na encje wygenerowane z XSD-ków i YAML-ów. Wymyśliłem to na samym początku i do tej pory rozwiązanie takie jest wystarczające. Jestem teraz w trakcie przeprowadzania małej rewolucji w modelu danych API (rozszerzanie danych zwracanych z endpointów, podział na metody do wyszukiwania encji i do pobierania pełnej encji, wraz z satelitami) i wymaga to tylko minimalnych zmian w warstwie repozytoriów.

    Plusem tworzenia zarówno SOAP-owych kontraktów, jak i REST-owych specyfikacji jest możliwość dotarcia do większego grona potencjalnych odbiorców Star Trek API. Zarówno SOAP, jak i Swagger pozwalają programistom piszącym w wielu różnych językach na stosunkowo tanie wygenerowanie klientów do API. Jakość takiego generowanego kodu może być różna, niemniej stanowi jakiś punkt wyjścia. Ponadto, jeśli klient do mojego API nie będzie działać, to issue na GitHubie powstanie w issue trackerze generatora kodu, a nie w moim projekcie.

    A teraz trochę o różnicach.

    SOAP pozwala dokładnie opisać zarówno obiekty zapytania, jak i obiekty odpowiedzi, którymi komunikujemy się ze światem zewnętrznym. IDEA, edytor którego używam na co dzień, posiada wsparcie dla WSDL-i i XSD-ków, więc wiele błędów wyłapuje bez konieczności uruchamiania buildu, chociaż, mówiąc uczciwie, IDEA posiada po prostu wsparcie dla walidacji na podstawie XSD, a namespace WSDL-a ma zalinkowanego własnego XSD-ka. Tym niemniej, SOAP to dojrzała technologia, posiadająca stabilny standard opisu obiektów i serwisów. Dodatkowo wsdl2java generuje interfejsy, które implementujemy, aby wystawić na zewnątrz endpointy. Większość popularnych błędów, które popełniałem przy pisaniu WSDL-i i XSD-ków objawiała się sensownymi komunikatami, po których było wiadomo, co powinno zostać poprawione.

    Z drugiej strony jest Swagger. Można w nim napisać encje, a z tych encji wygenerować kod javowy. Ta funkcjonalność działa. Można również wygenerować interfejsy, a nawet implementacje dla kodu serwerowego, ale po pobieżnym przejrzeniu kodu, który zostaje wygenerowany – a do wyboru jest tu kilka implementacji – postanowiłem wygenerować encje i klienty, ale napisać kod serwerowy samodzielnie, ponieważ generowały się głupoty.

    W Swaggerze każdemu adresowi w API odpowiada metoda w klasie klienta. Zaskoczeniem był dla mnie sygnatury generowanych metod. Wyobraźmy sobie bowiem endpoint do wyszukiwania, do którego można przekazać dwadzieścia albo trzydzieści kryteriów. Co robi wówczas Swagger? Generuje metodę z 30 parametrami! Powodzenia w refaktorowaniu takich metod, gdy pierwsza wersja klienta pójdzie już w świat – wówczas zamiana miejscami kilku argumentów tego samego typu będzie skutkować trudnymi do wychwycenia i naprawienia błędami. Podobnie będzie z dodaniem jakiegoś argumentu w środku już istniejącej listy. Dojście do tego, co właściwie się zmieniło i jak teraz powinno się używać klienta ma potencjał dostarczyć wielu godzin rozrywki. Spodziewałbym się, że klienty swaggerowe będą tworzyły osobne obiekty, które reprezentują parametry wejściowe endpointu, ale tak nie jest. Mam w planach przykrycie swaggerowego klienta czymś właśnie takim, by inni ludzie nie musieli zgadywać, co się zmieniło w moim API.

    Swagger w żaden sposób nie opracował wsparcia dla zagnieżdżonych obiektów w zapytaniach. Ostatecznie, po kilku próbach i błędach doszedłem do wniosku, że do transportu listy złożonych obiektów w zapytaniu najlepiej będzie mi na piechotę napisać serializator i deserializator. Być może przed ustabilizowaniem wersji beta wprowadzę w miejsce złożonych obiektów zwykłego JSON-a.

    Swaggerowe specyfikacje można napisać zarówno w YAML-u, jak i w JSON-ie, mamy więc wybór między dwoma wymiennymi formatami. Gdy uczyłem się pisać specyfikacje, było to dla mnie tylko utrudnieniem. Tutoriale, opisy błędów i StackOverflow pełne są przykładów działającego i niedziałającego kodu zarówno w YAML-u, jak i w JSON-ie. Sam zdecydowałem się na YAML i za każdym razem, gdy widziałem kod w JSON-nie, musiałem go sobie tłumaczyć w głowie na YAML, co przychodziło mi z mozołem. Rozumiem możliwość zapisania czegoś w więcej niż jednym formacie w przypadku Spring Batcha i jego definicji XML-owych i javowych, bo tam cele i ograniczenia są nieco inne. Rozumiem też to podejście w przypadku ORM-ów, które mogą korzystać z adnotacji na encjach albo z leżących obok tych encji XML-i (jak w przypadki Doctrine w PHP, gdzie na środowisku może po prostu być wyłączony Tokenizer, a więc w rezultacie adnotacje). Ale tutaj rozumiem mniej. W Swaggerze mamy dwa szeroko rozpowszechnione, równie czytelne dla ludzi i serializujące się tak samo formaty danych. Ale może to wybór poczyniony z tego samego powodu, dla którego ja piszę na raz API REST-owe i SOAP-owe – żeby więcej ludzi skorzystało z rozwiązania.

    Kolejnym minusem Swaggera jest wyjątkowo rozluźnione traktowanie błędów. Przykładowo, gdy specyfikując typ pola w encji wpisałem strin zamiast string, pole po prostu się nie wygenerowało, ale nie było żadnego komunikatu o błędzie, a na pewno nie taki, za którym idzie przerwanie buildu. Podobnie w przypadku, gdy pomyli się nazwy encji linkowanych w specyfikacji endpointów. Swagger po prostu wygeneruje kod javowy, który się nie skompiluje, bo klient API będzie wskazywał na nieistniejącą klasę.

    Rozpisałem się o minusach Swaggera, ale to nie jest tak, że nie da się go używać. Da się. Używam. Ale przy następnej okazji spróbuję RAML-a.

    Zaczynałem ten eksperyment bez żadnego biasu. Jedyną rzeczą, którą od początku się spodziewałem, było to, że barokowe XML-e będą zajmowały więcej miejsca, niż YAML-owe specyfikacje. Ale i to nie do końca się sprawdziło. Jeśli chodzi o miejsce zajmowane przez YAML-e w pionie, jest go nawet kilka procent więcej. Jeśli chodzi o miejsce zajmowane w poziomie, jest go oczywiście mniej.

    Nigdy nie miałem okazji budować API, które udostępnia te same operacje za pomocą REST-u i SOAP-u. Po napisaniu kilkunastu takich samych endpointów REST-owych i SOAP-owych nabrałem przekonania, że REST z użyciem Swaggera nie ma żadnych przewag nad SOAP-em. Natomiast przewagi SOAP-u na Swaggerem to, między innymi, możliwość wysyłania złożonych obiektów w zapytaniach, sensowniejszy generowany kod, lepsza walidacja, lepsze wsparcie w IDE i generalnie dojrzalszy i lepiej ustabilizowany ekosystem. Ale Swagger jest jak najbardziej używalny i na pewno się do niego nie zniechęcam, natomiast mam już teraz świadomość jego ograniczeń.