Porównanie Graphite i Prometheusa

Migracja z Graphite na Prometheusa wymusiła lepsze poznanie drugiego z nich. Pierwsze podejście do Prometheusa mnie zniechęciło. Niektóre koncepcje wydawały się niepotrzebnie skomplikowane.

W projekcie używamy Victoria Metrics – systemu który jest kompatybilny z zapytaniami Prometheusa (PromQL) z minimalnymi różnicami i dodaje sporo od siebie. Większość materiałów na które trafiałem w sieci dotyczyły Prometheusa dlatego zacznę od zestawienia Graphite z nim a później przejdę do porównania Prometheus z Victoria Metrics.

Zbieranie i przesyłanie metryk

Graphite w konfiguracji z której korzystam agreguje metryki po stronie aplikacji w dropwizard. Wartość Gauge jest wyliczana w momencie publikowania metryk z wykorzystaniem tylko jeden wątku. Więc jeśli ewaluacja wszystkich Gauge trwa dłużej niż minutę (np zapytania do bazy) to skutkowało lukami w metrykach.

Dla prometeusza jest podobnie z tą różnicą że wątki wyliczające wartość Gauga są przydzielane przez serwer http – to zwykła obsługa requestu. Czyli długo trwające pobieranie metryk nie uniemożliwia kolejnego odpytania (które prawdopodobnie też zablokuje zasoby na długi czas).

Instancja usługi z metrykami dla Prometeusza wystawia endpoint z aktualnymi wartościami metryk w tekstowym formacie (można je podejrzeć). Prometeusz pobiera metryki z wszystkich instancji (scrape)  co skonfigurowany okres – u mnie co minutę.

Rozsmarowuje te odpytania na całą minutę żeby rozłożyć obciążenie (siebie i sieci). W naszej konfiguracji Prometeusz także liczy percentyle po stronie aplikacji za pomocą micrometera.

Liczenie percentyli po stronie micrometera sprawia że zapytanie jest prostsze i szybsze. Minusem jest to że mamy tylko dostępne skonfigurowane wartości oraz nie możemy robić żadnych sensownych statystycznie operacji na kilku percentylach (może z wyjątkiem max i min). Wynika to z faktu, że tracimy liczebność próbek. Percentyl który powstał na podstawie jednej wartości i percentyl wyliczony z tysiąca są tak samo istotne. To samo dotyczy średnich.

Natywne histogramy Prometeusza nie tracą informacji o liczebności próbek. Dropwizard i StatsD (agregator danych który grupuje requesty do Graphite) mają typ histogramów ale graphite nie ma oddzielnych metod do operowania na nich. Histogram składa się z kilku metryk (jednej dla każdego bucketa). Każdy bucket zlicza wystąpienie wartości z określonego przedziału. Domyślnie bucket ma 12 przedziałów: 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 0.5, 10, nieskończoność. Ale można je skonfigurować tak żeby odpowiadały interesującym nas przedziałom. Nie korzystałem z nich.

Kolejny minus agregacji po stronie aplikacji to nieoczywisty zakres ich liczenia. Dokładnie wiemy jak daleko patrzą wstecz funkcje Prometeusza takie jak min_over_time, max_over_time i percentyle (quantile_over_time) bo look back window to jeden z elementów zapytania (więcej o tym dalej).

W graphite/dropwizard agregacje dotyczyły okna o stałej wielkości i wadze zmniejszającej się wraz z upływem czasu. To znaczy że dla ruchu (na minutę) większego od wielkości tego okna ignorowaliśmy requesty przy wyliczaniu p99. Zmniejszająca się waga próbki im dalej od jej pobrania sprawia że istotniejsze są te pobrane tuż przed przesłaniem metryk. To pozwala lepiej ocenić aktualny stan aplikacji ale zaburza wynik długotrwałej obserwacji.

Percentyle wyliczane przez micrometera dla Prometeusza wykorzystują HdrHistogram. Okna dla wyliczania to domyślnie 1 minuta ale wartości są grupowane w trzech histogramach po 20 sekund każdy. Rejestrowana wartość jest zapisywana do wszystkich trzech. Przy integracji z histogramem poprzedni jest czyszczony o ile minęło 20 sekund od poprzedniego czyszczenia.

Użycie trzech, według mnie, służy do tego żeby grupować wartości w oknach mniejszych niż 1 minuta, w przeciwnym razie wartość zarejestrowana w 1 sekundzie okna oraz ta z 60 sekundy razem by zostały usunięte w 61 sekundzie (następne okno). Wartość maksymalna jest czyszczona po 3 minutach ( ilość histogramów * wielkość okna (expiry)) co może być błędem (zgłaszane tu i tu) ale sprawia że maksymalna wartość jest bardziej wiarygodna – prawie na pewno trafi do Prometeusza.

Uwzględniając to, że scrapeowanie jest robione co minutę w wielu przypadkach wyliczone p99 nie będzie uwzględniało pełnej minuty – żeby tak było odczyt musiałby być przeprowadzony tuż przed rotacją histogramów. W odczycie zaraz po rotacji dane będą obejmowały tylko ostatnie 40 sekund.
Oba podejścia, w pewnych warunkach, nie uwzględniają wszystkich requestów przy wyliczaniu percentyli. Więc nawet jeśli patrzymy na p999 prawdopodobnie umyka nam więcej niż jeden pomiar na tysiąc. Plusem tego podejścia są łatwiejsze zapytania i mniejsze obciążenie prometeusza.

Zarówno dla Graphite i Prometeusza jeśli instancja padnie przed dostarczeniem metryk przepadają zmiany od poprzedniego przesłania.

Plusy Prometheusa w opisanej konfiguracji

  • możemy łatwo podejrzeć wartość metryk (i ciężej wstrzyknąć jakąś bokiem)
  • sprawdzamy czy server http instancji działa (ten sam który serwuje produkcyjny ruch), w push sprawdzamy tylko czy instancja jest w stanie wysłać dane
  • kilka niezależnych instancji Prometheusa może łatwiej otrzymywać metryki z jednej usługi
  • pobieranie metryk różnych instancji rozłożone w czasie
  • możliwość wyliczania percentyli po stronie serwera
  • wyraźniejsze wskazanie na jakim zakresie danych operuje funkcja

minusy:

  • prometheus musi mieć możliwość wykonywania requestów do instancji. Konfiguracje firewalli często umożliwiają ruch na zewnątrz a blokują do wewnątrz
  • prometeusz musi być spięty z service discovery (albo mieć zahardcodowaną listę instancji)
  • duży ruch i wysycenie puli połączeń przychodzących wyłącza możliwość odczytania metryk. Jeśli zmaleje to metryki zostaną w końcu odczytane chyba że k8s zrestartuje instancje bo ta nie odpowiada na Healthchecka

W sieci prometeusz przedstawia to zestawienie dość stronniczo. W naszej konfiguracji niezawodność dostarczania metryk wydaje się podobna. Tak samo obciążenie sieci i serwera zbierającego metryki. Ale tu powinien wypowiedzieć się ktoś kto utrzymywał oba systemy.

Victoria Metrics oficjalnie wspiera oba modele (push i pull), prometheus wspiera push tylko dla krótko trwających operacji które nie zdążyłyby by być zescrapowane ale są też nieoficjalne projekty które rozszerzają to zastosowanie.

Reprezentacja metryki w zapytaniu

Dla graphite to hierarchiczny string rozdzielony kropkami. Kolejne segmenty zawężają wynik zapytania. Funkcje operujące na segmentach posługują się ich numerem porządkowym liczonym od zera.

Jeśli nie chcemy filtrować żadnego segmentu musimy wpisać tam gwiazdkę. Pisząc zapytanie musimy wiedzieć za co odpowiada dany segment na podstawie dostępnych wartości. Podpowiadanie działa ok ale to trochę toporne zwłaszcza jak mamy dużo gwiazdek albo drzewiastą strukturę gdzie niektóre ścieżki są dłuższe i mają podobne segmenty co już występujące.

Prometheus to robi dużo lepiej: zapytanie to

 nazwa_metryki { label = “wartość”, label2!= “innaWartość” }

Funkcje operują na labelach a nie ich indeksach.

Podajemy tylko te labele/segmenty po których chcemy filtrować w dowolnej kolejności.

Wszystkie pozostałe dostaniemy bez potrzeby wpisywania gwiazdek. Nazwa metryki to też label o zastrzeżonej nazwie __name__ więc całe zapytanie możemy zapisać prawie jak JSONa

{__name__= nazwaMetryki, label = “wartość”, label2!= “innaWartość”}

Każda kolejna wartość labela (dowolnego) to nowa metryka ale dokładnie tak samo jest w graphite.

Może z mała różnicą że tam każdy label „pogarszał” zapytanie o kolejną gwiazdkę a tu można dodawać je bez wpływania na czytelność zapytania (ale z jakimś wpływem na utrzymanie klastra 😉 )

Gauge vs Counter

Graphite głównie korzystał z Gauge tzn wartości które mogły maleć lub rosnąć i oznaczały dane agregowane przez ostatnią minutę. Proste do zrozumienia i obsługi ale jeśli wysyłka się nie udała to dane z tej minuty przepadały. W naszych usługach nie widziałem takiej sytuacji prawdopodobnie przez to, że Graphite wysyłał metryki po TCP które ponawia pakiety dla których nie dostanie potwierdzenia odbioru, dodatkowo kod publikujący w przypadku błędu (np zakończenie połączenia z Graphite) odkładał niewysłane metryki i ponawiał ich wysłanie za minutę.

Prometheus zachęca do stosowania Counterów. Czyli wartości które nie mogą się zmniejszyć i agregują się przez cały czas życia instancji. Przykładowa metryka api_seconds_count to counter który podaje łączny czas obsługi requestów, labele pozwalają na pobranie wartości per endpoint, instancja, kod odpowiedzi itp

Wyjątek to restart instancji która zaczyna od zera. W obu przypadkach nie wysłane dane przepadną ale niektóre funkcje prometeusza dodadzą sobie do poprzedniej wartości aktualną. Wykrycie restartu jest robione podczas zapytania które operuje na counterach.

Graphite miał np nonNegativeDerivative która liczyła różnice między kolejnymi wartościami, jeśli to był counter to po restarcie funkcja zwracała 0 co zabezpieczało przed bezsensownymi ujemnymi wartościami ale skutkowało utratą różnicy dla momentu restartu.

Niegubienie danych zawsze na plus ale:

  • jeśli bardzo zależy nam na tym żeby móc śledzić każde wystąpienie jakiegoś zdarzenia i minutowa luka w danych jest niedopuszczalna to chyba nie powinniśmy tego robić metrykami (i tak możemy stracić dane)
  • to komplikuje zapytania (zaraz do tego wrócę)

Instant Vector vs Range Vector

Jedna z głównych przyczyn porażki mojego pierwszego podejścia do prometeusza.

Instant Vector

Wynik który zawiera maksymalnie jedną wartość (Sample) per metryka. To najbliższa wartość „przed” podanym timestampem (który nie musi być aktualnym czasem).

Prometheus patrzy 5 minut wstecz jeśli nie znajdzie żadnej wartości to nie zwraca metryki. Ten okres jest konfigurowany globalnie –query.lookback-delta w prometeuszu oraz -search.maxLookback w VMa. Według mnie ten czas powinien być powiązany okresem odczytu, który może różnić się per metryka więc prawdopodobnie sensownie opierać się na największym z interwałów.

Tabelaryczny podgląd próbek bezpośrednio w prometeuszu pokazuje odczyty wraz z timestampami (po @).

Grafana też na to pozwala ale trzeba zaznaczyć Table view na górnej belce, Format Table i Instant na dolnej. Ale jak już się uda to przedstawia czytelniej timestampy i umożliwia sortowanie.

Range Vector

Jak wyżej ale konfigurujemy okno samemu w nawiasach kwadratowych i zwracane są wszystkie sample a nie tylko ostatni.

Prometheus typuje wyrażenia więc jeśli Range Vector ma tylko jedną wartość to nie czyni go Instant Vectorem

W dokumentacji Prometheusa kilkukrotnie pada że przedstawiać na wykresie można tylko Instant Vector, rysowanie Range Vectora jest niemożliwe/niezdefiniowane. Internet powtarza to jak mantrę ale dla mnie jest dokładnie odwrotnie: Z jednej wartości nie zrobimy wykresu ale z listy wartości razem z timestampami jak najbardziej. To o czym Internet nie wspomina tak często to fakt że prometheus wystawia dwa endpointy do zapytań. Nawet dokumentacja prometeusza wspomina o tym daleko od stwierdzeń co może być rysowane.

query (Instant query)

Wbrew pozorom nie musi zwracać Instant Vectora… Znaczy tylko tyle że zapytanie będzie ewaluowane tylko dla jednego momentu w czasie (Instanta).

Vector jest oznaczany w odpowiedzi jako typ Vector, Range Vector jest zwracany jako matrix…

Request składa się z query i timestampa który można pominąć i prometeusz ustawi go na aktualny czas. Zaznaczenie w Grafanie Instant query korzysta z tego endpointu.

query_range (Range query)

Request wykonywany podczas rysowania wykresu. Składa się z zapytania, czas początku, końca oraz step/interval. Ostatni parametr jest ustawiany automatycznie przez Grafanę w zależności od tego jak szeroki wybrano przedział czasu ale można na nie wpłynąć ręcznie. Zapytanie jest wykonywane wielokrotnie do narysowania jednego wykresu (robiony jest tylko jeden request). Rezultat musi w wyniku zwracać Instant Vector (czyli „odwrotnie” niż sugeruje nazwa zapytania). Jeden punkt dla jednej ewaluacji. Czas początku i końca nie ogranicza pobieranych danych. Pierwsza ewaluacja odbywa się dla początku zakresu ale w zapytaniu może być pobierany dowolnie duży Range Vector(!)

Zakres resolution step z ilustracji poniżej to nie to samo co zakres Range Vectora, ale powinny być ze sobą powiązane o tym później.

Instant Vector – wiele metryk każda po jednej wartości

Range Vector – wiele metryk wiele wartości

Instant query – jedno wykonane zapytanie o dowolnym rezultacie. Do wszystkiego poza wykresami

Range query – zapytanie wykonane wielokrotnie każde o rezultacie Instant Vector. Tylko do wykresów

Czy to nie wygląda jak przypadkowa złożoność?  To według mnie dobry przykład czemu dobre nazewnictwo i abstrakcje są istotne.

Pod spodem oba wykonuje ta sama metoda tylko z innymi parametrami (instant ustawia timestamp jako start oraz koniec i ustawia range na 0).

Jakie są skutki powyższych rozwiązań z perspektywy użytkownika?

  • Każdy metryka typu counter musi występować z funkcją która zrobi z niej coś użytecznego (rate, increase i podobne)
  • poza początkiem i końcem zakresu (prawy górny róg grafany) oraz częstotliwością scrapeowania musisz ZAWSZE zastanowić się nad wielkością okna w Range Vectorze. Wielkość tego okna powinna zależeć od wybranego zakresu dat (jak jest duży) a konkretnie być powiązana z parametrem step/interval.

Jeśli będzie za mały to zignorujemy dane i będziemy mieli fałszywy obraz. Jeśli będzie za duży to nie będziemy widzieli krótko trwających problemów bo zostaną uśrednione. Jeśli będzie w sam raz to zoom in/out to popsuje (zmieni step).


Żeby increase (zmiana wartości – więcej o funkcjach niżej) działał dobrze potrzebuje przynajmniej 2 sampli w oknie.
Range nie może być równy wartości step bo wtedy okno będzie miało max 1 wartość. Nie może być równy 2x step ponieważ scrape interval nie jest przestrzegany idealnie. Czasem metryki będą odczytane trochę wcześniej a czasem trochę później i mogą się ułożyć tak że w okresie 2x step będzie tylko jedna wartość.
Jeśli to będzie 3x step to brak jednego sampla i przesunięcie drugiego będzie skutkowało tylko 1 samplem w oknie…

Prometheus nie daje możliwości ustawiania okna w zależności od stepu. Taką możliwość daje grafana. Twórcy wychodzą z założenia że powyższe problemy dotyczą tylko rysowania wykresów więc soft od wykresów powinien się tym zajmować.

Jeden z twórców Prometheusa pracuje dla Grafany i dodaje funkcje które według mnie są obejściem niedoskonałości Prometeusza. Zmienna $__interval daje dostęp do stepu z jakim rysowany jest wykres ale prometheus nie obsługuje działań arytmetycznych w oknie [5 * $__inverval] nie przejdzie więc dodali jeszcze
zmienną $__rate_interval oparta o scrape_interval i step żeby zaadresować powyższe problemy:

The value of $__rate_interval is defined as max($__interval + Scrape interval, 4 * Scrape interval), where Scrape interval is the “Min step” setting (also known as query*interval, a setting per PromQL query) if any is set. Otherwise, Grafana uses the Prometheus data source’s “Scrape interval” setting.

  • step niższy niż scrape interval skutkuje zwróceniem (i wyświetleniem) wielu „sztucznych” punktów. Które są wielokrotnie powtórzoną wartością ostatniego sampla. Trudniej w takiej sytuacji oddzielić rzeczywiste odczyty od ich powtórzeń. Zwłaszcza jeśli wartość metryki się nie zmieniła. W graphite podobnie działa funkcja keepLastValue ale można jej nie używać,  mieć luki w danych i tylko rzeczywiste sample
  • jeśli koniec okna nie pokrywa się z samplem to funkcje takie jak rate i increase estymują wartości na końcu okna na podstawie późniejszych wartości (więcej o tym dalej)
  • dla odmiany na plus: jest jasne jak szerokiego zakresu dotyczą wartości min_over_time, max_over_time i  percentyle tj quantile_over_time (bo sami je podajemy)

Graphite keeps it simple

Graphite robi to prościej: pobiera wszystkie sample od startu do początku zadanego zakresu. Jeśli chcemy poznać różnicę między kolejnymi wartościami to funkcja derivative patrzy na następny/poprzedni punkt bez koncepcji okna i niezależnie od tego jak daleko te punkty są od siebie.

Niektóre funkcje zapewniają agregacje w oknach np summarize który przyjmuje wielkość okna i funkcję agregującą. Ale graphite nie wymusza wykonywania agregacji żeby policzyć RPSy czy zmiany wartości licznika. Nie ma Instant/Range Vectorów są tylko listy wartości.

Operacje na metrykach

Funkcje operujące w poziomie lub pionie. Nie ma w PromQL takich które działają w obu płaszczyznach. Warto rzucić okiem na to zestawienie.

W poziomie/na jednej metryce – Victoria Metrics mówi o takich funkcjach „rollup” –

Liczniki:

increase – różnica między początkiem a końcem okna, uwzględnia reset licznika

rate – increase podzielony przez wielkość okna czyli przyrost na sekundę, uwzględnia reset

irate – rate który patrzy tylko na dwie ostatnie próbki (nawet jeśli okno jest dużo większe), uwzględnia reset. Teoretycznie służy do przedstawiania szybko zmieniających się wartości co lepiej pokaże ich zmienność (bez uśredniania). W praktyce oznacza że większość danych jest ignorowana a rezultat zależy od tego które próbki będą dwoma ostatnimi w oknie.

changes – ile razy się zmieniła wartość

resets – ile razy miał miejsce reset licznika

<aggregation>_over_time

avg_over_time, max_over_time, quantile_over_time, count_over_time

Funkcje patrzą na wszystkie wartości w oknie i na ich podstawie obliczają średnią, maksymalną wartość, zadany percentyl lub liczą ile razy występuje każda z wartości

Gauge:

delta – różnica między wartością z końca i początku okna

idelta – różnica między dwoma ostatnimi wartościami okna

deriv – rate dla gaugy

Zmiana wartości otrzymanej w powyższych funkcjach (wg VictoriaMetrics Transformation functions)

np. abs, ceil, histogram_quantile, label_replace, log10, round, sqrt

Niektóre funkcje w pionie/agregujące pojedyncze wartości różnych metryk. Wg prometheusa to operatory agregacji:

  • sum
  • min
  • max
  • avg
  • stddev (odchylenie standardowe)
  • count (ilość próbek)
  • count_values – odpowiednik count_over_time
  • topk(k) – zwrócenie tylko k pierwszych metryk, analogicznie bottomk
  • quantile – percentyl

Kolejność tego opisu odpowiada kolejności zapisu i wykonania operacji w PromQL

  • rollup [ Range Vector ]
  • transformacja [ Instant Vector ]
  • agregacja [ Instant Vector ]

To zakłada że nie można zrobić transformacji na Range Vectorze nawet jeśli taka operacja miałaby sens. Część agregacji ma swoje odpowiedniki w Rollupach tyle że z suffixem _over_time.

max_over_time zwróci największą wartość z okna jednej metryki (poziom) a max zwróci wartość największej metryki (pionowo).

Istnienie oddzielnego endpointu query_range który udaje że jest do czegoś więcej niż grafów sugeruje że będzie można powyższą kolejność powtórzyć kilkukrotnie (co step) a później zagregować wyniki każdego ze stepu (które są instant vectorami więc typy się zgadzają). Ale tak nie jest i nie da się tego zrobić w ten sposób. Do wersji 2.7 można było osiągnąć ten efekt tylko przez zastosowanie rules czyli zapytań które generują metryki na podstawie innych metryk. Coś w stylu widoku w bazach danych tylko obliczanego w momencie dostarczania danych. Po wersji 2.7 doszły jeszcze podzapytania.

Podzapytania

Jeśli chcemy wielokrotnie wykonać jakieś zapytanie (dla różnego czasu) a następnie zagregować wyniku to mamy do tego dodatkową składnie

increase(prometheus_http_requests_total[5m])[1h:5m]

Ten dodatkowy nawias kwadratowy oznacza: wykonuj te zapytanie ze stepem 5m cofając się o godzinę. Czyli dokładnie to co robi range_query. Range query nie potrafi zrobić nic innego z tymi danymi (jest w końcu tylko do rysowania) a podzapytania mogą np wybrać największą wartość

max_over_time(increase(prometheus_http_requests_total[5m])[1h:5m])

Można nie podawać stepu [1h:] będzie wtedy równy default evaluation interval propertiesowi określającemu jak często wykonywane są zapytania odpowiedzialne za wykonywanie rules (tych widoków). Domyślnie 15s.

Podobnie jak przy range_query z niepasującym step tak samo podzapytania z zakresem niepodzielnym przez step skróci zakres, [10m:6m]

Graphite keeps it simple again

Graphite również ma oddzielne funkcje dt jednej metryki i kilku metryk. Ale wynik funkcji to dla niego po prostu liczba lub kilka liczb. Można je komponować w dowolnych konfiguracjach.

Wszystko jest realizowane przez funkcje – okna jak w Range Vector także np

summarize(nonNegativeDerivative(groupByNodes(stats.tech.obfuscated.consumer.*.status.obfuscated.*.package-status-tracker.2xx.20{0,1}.count, 'sumSeries', 7)), "1h", "sum", true)

Metryka to ilość odpowiedzi 200 i 201 zwróconych szynie danych przez usługę package-status-tracker zsumowanych według wszystkich segmentów poza 7 czyli nazwą eventu. Następnie funkcja nonNegativeDerivative wyznacza różnice między kolejnymi wartościami tak increase w PromQL ale z nieco gorszym wsparciem dla resetu. Graphite nie interesuje jak odległe są od siebie kolejne wartości. Najbardziej zewnętrzna funkcja sumuje te przyrosty w godzinne okna. Wszystkie pośrednie etapy przetwarzania mają ten sam typ.

Rate i Increase

Increase mówi o ile licznik zwiększył się w zadanym czasie, w naszych serwisach najczęściej sprawdzamy przyrost z ostatniej minuty (co tyle jest scrape). Rate analogicznie podaje przyrost na sekundę i chyba najczęstsze wykorzystanie to wyliczanie RPSów. Ta funkcja uwzględnia resety licznika. Tzn w czasie przetwarzania zapytania sprawdza wszystkie kolejne pary punktów i jeśli wykryje że kolejny jest mniejszy to do wyniku dodaje wartość poprzedniego.

Jeśli pierwsza i ostatnia wartość w oknie (te które są wykorzystywane do wyliczania rate i increase) są odległe od granicy okna o mniej niż 110% średniej odległości między samplami to prometeusz robi ekstrapolacje (zakłada że obserwowany przyrost będzie się zachowywał tak samo na granicy okna).

Nie widziałem żadnych optymalizacji minimalizujących pobierane punkty czyli prawdopodobnie nawet jeśli wybierzemy bardzo duże okno to znaczenie mają tylko dwie wartości.

Zwłaszcza przy increase to może być nieoczywiste czemu wartości całkowite czasem mają ułamkowe przyrosty. Według mnie wynika to z przerzucenia na użytkownika konieczności ustalania okna, twórcy zapewniają że taki ułamkowy increase dalej jest statystycznie prawdziwy.

Twórcy VM uznali że aproksymowanie danych które już mamy jest bez sensu i jeśli w oknie nie zmieściła się jakaś wartość to VM patrzy nieco wstecz żeby ją pobrać. Nawet jeśli i tak jej nie znajdzie to nie jest robiona żadna aproksymacja.

W prometeuszu jest jeszcze funkcja irate wg dokumentacji

irate should only be used when graphing volatile, fast-moving counters

Zmiany w stosunku do rate są dwie: nie ma żadnych aproksymacji, ale brane są tylko dwa ostatnie sample. Co dodatkowo komplikuje zależność między step a wielkością okna. Możemy dobrać odpowiednie wartości (albo skorzystać ze zmiennych Grafany), później każdy step może pobierać dane z ogromnego okna po to żeby skorzystać tylko z 2 ostatnich punktów. Pozostałe mogłyby nie istnieć.

Filtry

Filtry usuwają wartości które go nie spełniają.

api_seconds{service="package-status-tracker", quantile="0.99"} > 0.1

Pozostawi tylko metryki większe niż 100 ms (metryka jest w sekundach).

Dodanie Bool po operatorze zmienia zachowanie – po filtrowaniu pozostaną wszystkie metryki, te które spełniają warunek otrzymują wartość 1 pozostałe 0. Przydatne np przy takim zapytaniu

avg(api_seconds{service="package-status-tracker", quantile="0.99"} > bool 0.01)

które zwróci proporcje próbek spełniających warunek do wszystkich.

Graphite ma mniej wygodne rozwiązanie –  funkcje.  Np  filterSeries, grep, threshold, removeBetweenPercentile.

filterSeries(system.interface.eth*.packetsSent, 'max', '>', 1000)

Z drugiej strony ma też filtry które operują nie na seriach tylko poszczególnych samplach removeBelow/AboveValue  (a później można wywołać funkcję która działa na całych seriach)

Vector matching

Gdzie chodzi tylko o Instant Vector (oczywiście duh…)

Jeśli chcemy wartość wszystkich metryk przemnożyć/odjąć/dodać… do stałej to zapis jest oczywisty metryka * 3. Po takiej operacji przepadnie nazwa metryki ale labele zostaną a wartość będzie odpowiednio zmieniona

Możemy robić te arytmetyczne operacje na dwóch Instant Vectorach ale muszą mieć dokładnie takie same labele (nazwa metryki może być różna). Jeśli metryka po lewej nie ma pary to jest odfiltrowana. Jeśli chcemy zezwolić na operację mimo różnic w labelach to musimy skorzystać z on (lista labeli które muszą się zgadzać) ignore (lista label które mogą się różnić)

Jeśli jedna metryka po lewej „pasuje” do kilku po prawej to musimy użyć group_left żeby potwierdzić że to zamierzone. Jest też group right a obie mogą przyjmować listę labeli które mają być skopiowane do lewej metryki.

Graphite ma do tego metodę applyByNode

applyByNode(stats.counts.haproxy.*.*XX, 3, "asPercent(%.5XX, sumSeries(%.*XX))", "%.pct_5XX")

nigdy jej nie używałem. W prometeuszu ta operacja wydaje się czytelniejsza.

Set operators

Prometheus nazywa to operacjami na zbiorach ale można to rozumieć jako instrukcje warunkowe

  • zapytanie and drugie_zapytanie: wyświetla metryki które są wynikiem pierwszego zapytania o ile mają odpowiednik o dokładnie takich samych labelach w drugim (nazwa drugiej metryki i wartość jest nieistotna)
  • zapytanie or drugie_zapytanie: robi to co and ale wyświetla wyniki drugiego_zapytania bez pary
  • zapytanie unless drugie_zapytanie: wyświetla tylko wynik zapytania bez pary

Nie używałem, tego typu funkcje są potrzebne raczej tylko w czujkach a nasz system do czujek ma swoje odpowiedniki

Grafana

Dla graphite pozwala na odniesienia do kolejnych zapytań przez #A #B itd. To pozwala na rozbicie skomplikowanego zapytania na mniejsze i czytelniejsze części. Prometeusz niestety tego nie ma ale VM ma możliwość przypisywania zapytań do zmiennych w wyrażeniach WITH

Prometheus ma zmienne i parametry nieobecne dla graphite:

  • __range: wielkość zakresu wybranego w prawym górym rogu grafany w sekundach
  • __rate_interval: wartość większa niż rate (opisana wcześniej) dla bardziej odpornego na braki w metrykach obliczania rate/increase

Legend format – przez odniesienia {{nazwa_labela}}.{{inny_label}} pozwala na modyfikowanie jak ma być wyświetlana metryka bez stosowania funkcji operujących na labelach

Min step – do wpływania na parametr step. Według mnie niepotrzebne poza debugiem i próbą zrozumienia jak działa zapytanie

Resolution – też wpływa na step, pozwala go ustawić na mniejszą wartość niż sugeruje wybrany zakres

Format – time series/table/heatmap  nie wiem czemu Grafana wymaga wybrania przy zapytaniu atrybutu który dotyczy prezentacji i powinien być ustawiany tylko przez Visualisation

Instant – wysyła request do endpointu query

Exemplars – możliwość korelacji metryk z logami. Nie znam szczegółów – nie mamy takiej opcji

Zmiany w Victoria Metrics

Pełna lista zmian TUTAJ. Najważniejsze według mnie:

  • VM nie robi aproksymacji przy rate/increase i innnych funkcjach które liczą różnicę między próbkami, pobiera trochę więcej danych i bazuje na nich. Konkretnie patrzy wstecz 5 minut i jeszcze odejmuje step lub window (w zależności co jest większe). Dlatego dzięki VM nie widzimy ułamkowych wartości increase dla liczników z wartościami całkowitymi
  • ma wbudowaną analizę zapytania (trzeba wybrać trace query)
  • stara się unikać nazewnictwa Instant/Range Vector, skupia się na rollup/transform/aggregate
  • pozwala odnosić się w zapytaniu do wartości step. Grafana też na to pozwala ale nie wszystkie zapytania odbywają się przez nią
  • pozwala pomijać wielkość okna i podawanie funkcji rollup. Co stara się zdjąć z użytkownika konieczność znania szczegółów PromQL. Nie jestem pewny czy nie idzie z tym zbyt daleko.

Trzeba pamiętać, że jeśli zapytania przechodzą przez proxy które je parsują (np promxy) to dostępność nowych funkcji zależy od tego czy proxy wspiera MetricsQL.

Class Loadery


Po co powstały

Żeby nie wiązać się ze sposobem dostarczania do JVMa kodu klasy. Można go ściągać, odszyfrowywać albo generować w locie. Umożliwiają instrumentalizację kodu (debugger). Klasy załadowane przez sąsiednie albo potomne Class Loadery są wzajemnie niewidoczne (bezpieczeństwo). Pozwala na załadowanie kilkukrotnie różnego kodu pod tą samą nazwą klasy ale nie wiem czemu ktoś miałby to robić.

Co robią

Dostarczają JVMowi bajty klasy w ściśle zdefiniowanej postaci. Same nie tworzą obiektu Class (te javowe). Robi to natywny kod który sprawdza poprawność i kompatybilność binarną.

Linkowanie

Referencje do obiektów są określane w czasie wykonania a referencje do klas w czasie linkowania. Linkowanie może być robione wcześnie (tuż po kompilacji) albo późno (w czasie wykonania). Java linkuje tak późno jak to możliwe. Typ jest określany przez kwalifikowaną nazwę klasy oraz CL który ją załadował.

Linkowanie jest robione przy wykorzystaniu Class.forName
nie jest robione przy loadClass z Class Loadera – jest przeciążona metoda z flagą loadClass(String name, boolean resolve) ale jest protected.

Klasa do załadowania swoich zależności używa CL który ją załadował.

Inicjalizacja

Klasa która jest zlinkowana ma w Constant Pool mapowania kwalifikowanych nazw klas na ich definicję (Class). Definicja klasy niekoniecznie musi być zainicjalizowana. Tzn. jej statyczne bloki inicjalizujące mogą nie być wykonane tak samo jak przypisania do statycznych pól. Jeśli w wyniku wykonania kodu JVM musi linkować kod to także go inicjalizuje.

Użycie stałej NazwaKlasy.class wykorzystuje metodę loadClass Class Loadera która nie inicjalizuje. Analogiczny kod w kotlinie inicjalizuje companion object. Przeciążone Class.forName(String name, boolean initialize, ClassLoader loader) przy przekazaniu false nie linkuje

Kompatybilność binarna

Przy kompilowaniu klas/modułów oddzielnie klasa która wykorzystuje inną klasę mogła być kompilowana z inną definicją swojej zależności niż ta aktualnie dostępna na Class Pathu. Nie wszystkie zmiany spowodują błąd. Niektóre kompatybilne zmiany [JLS]:

  • Reimplementing existing methods, constructors, and initializers to improve performance.
  • Changing methods or constructors to return values on inputs for which they previously either threw exceptions that normally should not occur or failed by going into an infinite loop or causing a deadlock.
  • Adding new fields, methods, or constructors to an existing class or interface.
  • Deleting private fields, methods, or constructors of a class.
  • When an entire package is updated, deleting package access fields, methods, or constructors of classes and interfaces in the package.
  • Reordering the fields, methods, or constructors in an existing type declaration.
  • Moving a method upward in the class hierarchy.
  • Reordering the list of direct superinterfaces of a class or interface.
  • Inserting new class or interface types in the type hierarchy.

Podobna sytuacja występuje dla serializacji.

Implementacja Class Loaderów

Obiekt Class powstaje w wyniku wywołania jednej z wersji natywnej metody defineClass w ClassLoader. Class ma referencje do Class Loadera który ją załadował. Dlatego ten sam kod klasy załadowany przez dwa różne ClassLoadery skutkuje dwoma typami (między którymi nie można rzutować).

Natywne metody ładujące są prywatne i są wywoływane przez protected defineClass które robi walidacje certyfikatów o ile są i nazwy ładowanej klasy (np czy pakiet nie zaczyna się od java żeby nie można było załadować swojej wersji Stringa). Bez Security Managera prywatne metody można zawołać refleksją. Przewidziany przez twórców sposób to rozszerzenie klasy ClassLoader i wywołanie metod z walidacją.

Hierarchia Class Loaderów

Klasa Class Loader zawiera metodę szablonową loadClass(String name, boolean resolve) która zakłada locki w przypadku ładowania wielowątkowego (tylko jeden wątek ładuje klasę z wykorzystaniem jednego CL) oraz zapewnia domyślną hierarchiczność CL. Metoda do zaimplementowania to findClass(String name) która musi zwrócić klasę więc w implementacji zawołać którąś z natywnych defineClass. Domyślna hierarchia to :

  • Bootstrap
  • Extension
  • Application/System

Loader najpierw deleguje do rodzica zanim spróbuje załadować sam. Zapewnia to że klasy core’owe będą ładowane tylko raz i to przez Bootstrapowy CL oraz że wszystkie klasy odnosząc się do Stringa dostaną tą samą klasę Stringa.

Bootstrap

Bootstrapowy CL jest napisany w C i nie można mieć referencji do niego z poziomu Javy. Ładuje klasy z JRE. Powtarza część walidacji robionych w javie i dodaje swoje (dt. spójności typów [Bracha])

Extension

Extension CL powstał na potrzeby Apletów (jak być może cały mechanizm CL) i daje alternatywny/globalny „Class Path” czyli miejsce z którego są ładowane klasy dla wszystkich JVMów na danej maszynie o ile nie zmieniały parametrów uruchomieniowych (java.ext.dirs które powinno zawierać foldery a nie pliki).

Extension CL ładuje pliki z pominięciem walidacji Security Managera (? AccessController.doPrivileged() i nie sprawdza uprawnienia createClassLoader).

System

Systemowy CL ładuje kod z Class Patha.

Problemy

Nie zawsze CL działają parent first, servlety są child first:

„The Web application class loader must load classes from the WEB-INF/ classes directory first, and then from library JARsB in the WEB-INF/lib directory.” str 109 spec 3.0

To może powodować problem jeśli klasa możliwa do załadowania przez rodzica i potomnego. Kod rodzica będzie używał innej klasy niż kod potomny i nie będą mogły przekazywać instancji tej klasy w wywołaniach między sobą.

Przy ładowaniu parent first problem pojawia się jeśli kod core’owy (rodzica) korzysta z klas na Class Path np. Xerces (xml), JNDI (service locator).

Przy każdym sposobie ładowania pojawia się problem współpracy między kodem załadowanym przez różne CL. Musi się to odbywać przez interfejs który jest wspólny dla obu (ładowany prawdopodobnie przez trzeciego CL do którego oba mają referencje).

ContextClassLoadery

Żeby zignorować hierarchie wprowadzono ContextClassLoadery. To pole w Thread do którego można wsadzić Class Loadera (żeby nie trzeba było do Thread Locala). A później w tym samym wątku ale w klasie załadowanej przez innego CL zignorować jej aktualnego CL i użyć tego z wątku.

Źródła:
„The Java® Virtual Machine Specification” Tim Lindholm, Frank Yellin, Gilad Bracha, Alex Buckley

„Inside the Java Virtual Machine” Bill Venners

Dynamic Class Loading in the Java VirtualMachine Sheng Liang, Gilad Bracha

Dokumentacja mechanizmu rozszerzeń

Specyfikacja Servletów 4.0

Ogólnie o ładowaniu klas

O Context Class Loaderach

Rożnica między Class.forName() i ClassLoader.loadClass()

JLS Kompatybilność binarna


Functional Programming in Java

Okładka Functional Programming in Java

Function

Ma domyślne metody:

funkcja.compose(wcześniejszaFunkcja)składa wywołania funkcji, parametr będzie najpierw przekazany do wcześniejszejFunkcji a wynik do funkcji

funkcja.andThen(późniejszaFunkcja) przekaże parametr do funkcji a rezultat do późniejszejFunkcji

funkcja.identity() zwraca przekazany parametr

Predicate

Ma domyślne metody and(innyPredykat), or(innyPredykat) i negate() oraz statyczną isEqual(obiekt) która zwraca predykat porównujący, z wykorzystaniem equals, z przekazanym obiektem.

Unary/Binary Operator

Rozszerzają odpowiednio Function i BiFunction ale operują tylko na jednym typie generycznym

UnaryOperator ma identity() który zwraca ten sam UnaryOperator a BinaryOperator ma minBy(comparator) oraz maxBy(comparator) które porównują dwie wartości.


Checked Exceptions

Interfejsy z java.util.function nie deklarują żadnych sprawdzanych wyjątków dlatego muszą być one łapane lub przepakowywane wewnątrz lambdy. Ewentualnie można utworzyć interfejs który deklaruje wyjątki ale i tak nie będzie możliwy do użycia w streamach:

interface UseInstance<T, X extends Throwable> {
void accept (T instance) throws X;
}

Jeśli koniecznie z lambdy musi wydostać się sprawdzany wyjątek wtedy:

  • unsafe.throwException(new IOException()) działa
  • Thread.currentThread().stop(new IOException()) już nie działa
  • ograniczony parametr typu i rzutowanie działa:
private static <T extends Throwable> void sneakyThrow(Throwable exception) throws T {
throw (T) exception;
}

Stream

Streamy są leniwe więc zmiana źródła danych z którego powstał stream przed wywołaniem metody kończącej zmienia stream. Zmiana źródła w czasie przeglądania streamu jest bezpieczna tylko dla bezpiecznych wielowątkowo źródeł (ConcurrentHashMap).

Niektóre sposoby stworzenia streama:

  • Stream.of(varargs)
  • Stream.generate(supplier)
  • Stream.iterate(poczatkowaWartość, zmianaWartości)
  • Stream.concat(jakiśStream, innyStream)
  • Stream.builder().add("element").build()

ZmianaWartości w Stream.iterate() może polegać np. na zwróceniu następnej liczby pierwszej.

Ograniczanie streama przez limit i skip

Przejście do streamów prymitywnych przez mapToInt/Double/Long, są też wersje flatMapTo

Primitive Stream

Przejście do Streamu obiektowego przez mapToObject

Przy wyliczaniu średniej zwracają prymitywne Optionale np. OptionalDouble, OptionalInteger, OptionalLong – od zwykłego Optionala różnią się tym, że opakowują typ prosty i mają metodę getTypProsty, dodatkowo orElse wymaga Suppliera tego typu prostego.

sum na typie prostym zwraca wartość bez Optionala – dla pustego streamu zwraca 0

CharSequence (String, StringBuilder…) ma domyślną metodę chars która zwraca IntStreama. To Integery nie Charactery przez znaki uzupełniające Unicode które nie mieszczą się w zakresie char a kompatybilnośc wsteczna nie pozwala go rozszerzyć.

Summary Statistics

Dają dostęp do max, min, average, count
Do statystki można dodawać kolejne wartości (accept) albo łączyć kilka statystyk ze sobą (combine)
wszystkie interfejsy dla streamów prymitywnych mają metody asTypPrymitywny – wyjątki do consumer i predicate – te mają standardowe nazwy


Collectors

stream.collect(supplier, consumer, consumer) pierwszy dostarcza pusty obiekt drugi to akumulator a trzeci combiner
stream.collect przyjmuje też obiekt Collector który implementuje 3 powyższe metody plus finisher i characteristics (concurrent, unordered, identityFinish – czyli finisher nic nie robi).

toMap

toMap(keyMapper, valueMapper) przyjmuje dwie funkcje z których jedna to często Function.identity(), jeśli klucz jest zdublowany wtedy jest rzucany IllegalStateException

toMap(keyMapper, valueMapper, mergeFunction) jak poprzednia ale w razie wystąpienia duplikatu jest on mergowany za pomocą mergeFunction

toMap(keyMapper, valueMapper, mergeFunction, mapSupplier) pozwala wybrać typ mapy, domyślnie to HashMapa

Wszystkie trzy funkcje mają swoje bezpieczne wielowątkowo odpowiedniki: toConcurrentMap

groupingBy

groupingBy zwraca mapę w której wartością jest lista, groupingBy w groupingBy zwraca mapę map.

GroupingBy ma trzy warianty:

  • groupingBy(funkcjaKlasyfikująca)
  • groupingBy(funkcjaKlasyfikująca, collectorDlaDownstream)
  • groupingBy(funkcjaKlasyfikująca, fabrykaMapy, collectorDlaDownstream)

Wszystkie trzy mają też odpowiedniki bezpieczne wielowątkowo.

collectorDlaDownstream umożliwia dalsze przetwarzanie wartości w mapie przez zastosowanie dowolnego Collectora, jeśli klucz i wartość są na innych poziomach zagnieżdżenia, lub wymagają modyfikacji, przydatny jest Collectors.mapping, który mapuje wartości. W javie 9 dodano analogiczne flatMapping i filtering.

partitioningBy

Przyjmuje predykat i zwraca mapę która ma zawszę dwie wartości True i False (niezależnie czy mają jakieś wartości).

Ma tylko dwa warianty:

  • partitioningBy(predykat)
  • partitioningBy(predykat, collectorDlaDownstream)

Nie można określić rodzaju mapy, np wykorzystać wersję bezpieczną wielowątkowo. Nie ma bezpiecznych wielowątkowo metod partitioningBy.

Redukujące

  • joining()
  • joining(delimiter)
  • joining(delimiter, prefix, sufix)

Działa analogicznie do String.join(delimiter, stringi). Pierwszy łączy Stringi, drugi łączy i dodaje między nimi delimiter (poza ostatnim). Trzeci umożliwia poprzedzenie i zakończenie całego ciągu.

  • maxBy(comparator)
  • minBy(comparator)

Zwracaja Optionale i nie mają wersji z domyślną wartością tak samo jak ich odpowiedniki dla streama.

counting() – zwraca longa

Reduce

Od Collectora różni się tym, że typ zwracany musi być taki sam jak typ elementów i w teorii każde wywołanie tworzy nowy element. W praktyce nic poza konwencją nie stoi na przeszkodzie żeby reduce mutowało przekazywane mu obiekty i je zwracało. Dodatkowo w jednym przypadku zwracany typ może być inny niż typ streama.

Zarówno Collectors.reducing() jak i stream.reduce() mają trzy wersję:

  • reducing(redukującyBinaryOperator) – zwraca Optionala
  • reducing(wartośćPoczątkowa, redukującyBinaryOperator) – dla pustego streamu zwraca wartośćPoczątkowa, dla niepustego wartośćPoczątkowa jest pierwszym argumentem redukującegoBinaryOperatora
  • reducing(wartośćPoczątkowa, mapper, redukującyBinaryOperator) – jak wyżej ale przed rozpoczęciem redukcji najpierw wartość jest mapowana, ta sygnatura metody pozwala na redukowanie do typu wartościPoczątkowej która może być inna niż typ streamu, analogiczna metoda Stream.reduce(wartośćPoczątkowa, redukującaBiFunkcja, combiner) jest wykorzystywana przy przetwarzaniu wielowątkowym a combiner łączy rezultaty uzyskane w podzadaniach.

Arytmetyczne

Wszystkie zwracają Double, 0 jeśli stream jest pusty, czyli inaczej niż dla prymitywnych streamów gdzie jest zwracany odpowiedni Optional.

averangingDouble/Int/Long(maper Object -> odpowiedni typ prosty) -dają dokładniejsze wyniki przy ciągu rosnącym.

summarizingDouble/Int/Long() – zwraca statystyki bez konieczności przechodzenia na stream dla typów prostych

summingDouble/Int/Long()– prostsza wersja powyższego z samym sumowaniem

Do kolekcji

  • toList()
  • toSet()
  • toCollection(konstruktorKolekcji)

Dwa pierwsze dają niegwarantowane implementacje (ArrayList i HashSet), trzeci wymusza podanie konstruktora samemu

Wrapper

collectingAndThen(downstreamCollector, funkcjaOpakowująca) – wynik działania downstreamCollectora zostanie przekazany funkcjiOpakowywującej i to jej rezultat zostanie zwrócony. Funkcja może np. opakowywać kolekcję w Collections::unmodifiableCollection.

Przy implementowaniu Collectora samodzielnie funkcjaOpakowująca to finisher.

Typy proste

Mają typy ObjTypPrymitywnyConsumer która przyjmuje T i typ prosty i ma metodę accept.


Lambdy

Zmienne w lambdach domyślnie nie są finalne. Jeśli mają być wtedy trzeba podać też typ.

Pole jest zawsze Effectively final: 

class EffectivelyFinal {
int value = 1;

void method(){
value = 2;
produce(() -> value);
value = 3;
}

int produce(Supplier<Integer> supplier) {
return supplier.get();
}
}

Ten kod się skompiluje bo value jest polem i odniesienie ma postać this.value. A this jest Effectively final.

Jeśli funkcja zależy od zmiennej spoza niej (domknięcie) wtedy można zrobić funkcję która przyjmuję tą wartość i zwraca funkcję która ją wykorzystuje.
Można zawęzić zasięg wewnętrznej funkcji tylko do zewnętrznej (odpowiednik where w Haskelu) przez zwracanie lambdy jako wyniku lambdy.

final Function<String, Predicate<String>> startsWithLetter = 
letter -> name -> name.startsWith(letter);

Wykorzystanie:
...
.filter(startsWithLetter.apply("N"))

Comparator

Comparator ma domyślne metody: thenComparing(innyComparator) i thenComparing(funkcjaWyciągającaRzeczDoPorównywania, comparator)

W mapie są comparatory comparingByKey i comparingByValue zarówno w naturalnym porzadku jak i z wykorzystaniem przekazanego comparatora
są też comparingInt/Long/Double(przyjmuje ToIntFunction która wyciąga z obiektu typ prosty


Rekurencja ogonowa

Java nie optymalizuje rekurencji ogonowej. Wywołanie metody można przenieść ze stosu na stertę. Kolejne wywołania można przedstawić w postaci obiektu który albo jest potrzebny do kolejnego wywołania albo posiada wynik.

Obiekt może rozróżnić te 2 sytuacje i rzucać wyjątek jeśli jest pobierany wynik z nieukończonej rekurencji lub rekurencja jest wywoływana dla obiektu posiadającego wynik.

Liczba możliwych wywołań rekurencyjnych jest niedeterministyczna. Wątek wykonujący wywołania ściga się z JITem który kompresuje ramkę. Im wcześniej JIT ją skompresuje tym więcej wywołań będzie miało miejsce przed przepełnieniem stosu. W przypadku wyłączenia JITa przez -Djava.compiler=NONE ilość wywołań jest stała i zależna od wielkości stosu.


Memoizer

Funkcja która jest rekurencyjnie wywoływana ma referencje do samej siebie owrapowanej w coś co ją cacheuje.
Memoizer tworzy swoją implementacje Function która ma cache dla parametrów wywołania, jeśli nie ma wyniku w cache to wywołuje przekazaną funkcję ale podając referencje do siebie (wrappera). Przez co funkcja może wykorzystać wrapper wywołując się rekurencyjnie.

class Memoizer {
public static <T,R> R callMemoizer(final BiFunction< Function<T,R>,T,R> function, final T input ){
Function<T,R> memoized = new Function<T,R>(){
HashMap<T,R> store = new HashMap<>();
@Override
public R apply(T input) {
return store.computeIfAbsent(input, key -> function.apply(this, key));
}
};
return memoized.apply(input);
}
}

Nieco podobne do ThreadLocal, nie ma globalnego cache w którym trzeba szukać po wątku / funkcji i argumencie. A każdy wątek/funkcja ma swój cache. Nie sprawdzi się dla rekurencji liniowej (która woła się tylko raz) bo wartości będą dodawane do cache jak już będą niepotrzebne.

Źródła:
„Functional Programming in Java: Harnessing the Power Of Java 8 Lambda Expressions ” Venkat Subramaniam

Dokumentacja Streama

Dokumentacja Collectora

Pola i Effectively final (23 minuta)

Throwing Undeclared Checked Exceptions

TDD Test Driven Development: By Example

TDD zaczyna od końca

Opis systemu zaczyna się od opisywania funkcji które oferuje.
Opis funkcji zaczyna się od opisania pożądanego stanu którym skutkuje wywołanie funkcji.
Implementacje zaczyna się od napisania testów które wykorzystują tą implementacje.
Testy zaczyna się od pisania asercji która weryfikuje czy test przeszedł.

Cykl TDD

  • napisanie testu który nie przechodzi
  • napisanie kodu który przechodzi test jak najprostszym sposobem
  • refactor

Test nie przechodzi

Pierwsza część cyklu sprawdza założenia (być może warunek będzie już spełniony np. przez wykorzystywaną bibliotekę).
Istotne jest żeby być jak najkrócej w „CZERWONYM” etapie. To sprawia że skomplikowany problem jest rozbijany na mniejsze fragmenty o których łatwiej myśleć.
Istotna jest umiejętność robienia zmian w wielu krokach by utrzymać powyższy cykl. Jeśli coś jest oczywiste można skumulować kilka zmian ale jeśli testy nie przechodzą należy wrócić do małych kroków.
Zmniejszanie „CZERWONEGO” etapu skutukuje szybszą informacją czy poprzednia decyzja (najlepiej jedna) była słuszna.

Banalna implementacja

Druga część cyklu sprawdza zasadność testu – wyklucza pisanie skomplikowanej implementacji czegoś niepotrzebnego/błędnego. Jest to realizowane przez:

  • napisanie kodu który hardcoduje pożądaną wartość (Fake)
  • zaimplmentowanie oczywistego rozwiązania (Obvious Implementation)

Refactor

Trzecia wprowadza porządek. Wraz z dopisywaniem testów implementacja staje się coraz bardziej ogólna.
Jeśli kod dalej jest tylko sumą różnych przypadków może pomóc triangulacja czyli napisanie testów dla różnych danych (nie koniecznie skrajnych). To może uwidocznić algorytm do zaimplementowania.

Zbyt silne powiązanie testów z implementacją jest złe. Musi być możliwość dopisywania kolejnych testów bez zmiany kodu źródłowego.

Kolejny cykl

Należy zajmować się tylko jednym zagadnieniem na raz a pozostałe spisywać. Ze spisanych najlepiej wybierać te które wydają się proste do zrobienia (ale nie koniecznie te najprostsze – te nic nie wniosą).
Jeśli już praca nad zagadnieniem jest przerwana czymś pobocznym nie należy przerywać przerwania.
TDD i YAGNI są bardzo blisko związane. Nie należy pisać czegoś do czego nie ma testów a nie należy pisać testów do czegoś co nie jest wymagane w tym momencie.
Nowe funkcje można dodawać tylko w „ZIELONYM” etapie (żeby pracować tylko nad jedną rzeczą na raz).

Ogólne

TDD sprawia, że architektura jest dopasowana do aktualnych, nie przyszłych, wymagań.
Wątpliwości trzeba wyrażać w formie testu.
Zaczynać pisanie testów od przypadku który nie wymaga żadnego działania (zdegenerowany przypadek) lub dla najprostszych wariantów.

Dobrze pisać testy służące do nauki zewnętrznej biblioteki. Jak za długo nie ma postępu to zacznij od nowa.
Dobrze zostawiać czerwony test na następny dzień – będzie wiadomo od czego zacząć.

Źródła:
„Test Driven Development: By Example” Wydanie I Kent Beck

OCP Czas

UTC

Standard zapisu czasu, wykorzystujący uśredniony czas słoneczny zerowego południka. Nie mylić z GMT które jest strefą czasową, tym bardziej nie mylić z czasem londyńskim (po przejściu na czas letni Londyn ma przesunięcie +1). Do wyznaczania czasu w standardzie UTC wykorzystywanych jest ponad 400 zegarów atomowych które co jakiś czas są korygowane o sekundę przestępną.

Sekunda przestępna

W wyniku nieregularnego zwalniania obrotu ziemi wokół własnej osi czas UTC rozsynchronizowuje się z czasem słonecznym. By zniwelować tą różnicę ogłaszane jest wystąpienie sekundy przestępnej (z wyprzedzeniem). Dodatnia sekunda „cofa” czas przez wystąpienie 23:60. Ujemna sekunda przeskakiwałaby jedną sekundę ale jeszcze nie wystąpiła.
Sekunda przestępna przeczy założeniu, że minuta ma 60 sekund co może powodować błędy.
Istnienie sekundy przestępnej sprawia, że nie ma możliwości dokładnego obliczenia ilości sekund między dwiema datami bez informacji o sekundach przestępnych (wyjątek data Juliańska).

Java Time Scale

System zapisu czasy stosowany w Javie.

    • Założenia:
  • pokrywa się z czasem UTC w południe każdego dnia
  • ma dwie wersje: przed 1972-11-03 pokrywa się z czasem słonecznym zerowego południka, po pokrywa się z UTC-SLS

Implementacja UTC-SLS

Może być wykonana po stronie systemu na którym zainstalowany jest JVM albo w JVM.
Nie ma sekund przestępnych. Jeśli implementacja jest po stronie systemu wtedy czas systemowy jest zwracany w System.currentTimeMillis() a przez to w wyniku wywołania wszystkich now() na obiektach czasu. Jeśli tak nie jest JVM rozciąga czas trwania ostatnich 1000 sekund dnia o 1/1000 sekundy.

ZonedDateTime

Zawiera LocalDateTime i ZoneId. Wszystkie obiekty czasu są bezpieczne wielowątkowo i niezmienne.

Na podstawie ZonedDateTime można tworzyć instancje w innych strefach czasowych:
withZoneSameInstant(zone) po prostu przypisuje LocalDateTime inną strefę czasową
withZoneSameInstant(zone) mówi jaka godzina jest teraz w podanej strefie czasowej

LocalDateTime

Godzina i data bez strefy czasowej, zawiera LocalDate i LocalTime. Oba obiekty udostępniają metody do utworzenia LocalDateTime np. atTime(), atStartDay() lub atDate().
Zawierają metody until() służące do obliczania różnicy między obiektami w jednostkach podanych jako drugi parametr. Wykonanie jest delegowane do kodu jednostki ChronoUnit.
Zwracają zmodyfikowaną datę przez plusJednostka() i minusJednostka() np. minusSeconds(), plusWeeks().

ZoneId

Reprezentuje strefę czasową, która składa się z różnicy w stosunku do czasu GMT (ZoneOffset) oraz reguł zmiany czasu (ZoneRules).

ZoneOffset

Przechowuje różnice w sekundach cache’ując wielokrotności 15 minut oraz dotychczas użyte Offsety.

ZoneRules

Zawierają informację o zmianach czasu podzielone na dwie grupy:
ZoneOffsetTransitionRules algorytmiczne reguły zmiany czasu, np ostatnia niedziela marca
ZoneOffsetTransition dotychczasowe historyczne zmiany czasu oraz zmiany wynikające z zastosowania ZoneOffsetTransitionRules w danym roku, zawierają Offsety przed i po zmianie oraz czas zmiany.

Jeśli obiekt ZoneDateTime jest tworzony na podstawie LocalDateTime wtedy mogą wystąpić trzy przypadki:

  • normalny: dla tej daty i strefy czasowej występuje tylko jedno przesunięcie w stosunku do GMT
  • godzina nie istnieje: jest pomijana w czasie zmiany czasu na letni
  • godzina występuje podwójnie: zegar jest cofany przy zmianie czasu na zimowy

W drugim przypadku do „nieistniejącej” godziny dodawana jest wartość zmiany czasu.

W trzecim przypadku domyślnym zachowaniem jest ustawienie godziny ze „starym” przesunięciem. Można wpływać na to zachowanie metodami withEarlierOffsetAtOverlap (domyślne zachowanie) oraz withLaterOffsetAtOverlap. Metody zwrócą nowy obiekt z alternatywną wartością przesunięcia. Earlier znaczy „wcześniejsze: (przed zmianą czasu) nie „o mniejszej wartości”. Alternatywnie można wykorzystać parametr prefferedOffset – jeśli wśród praw przesunięć będzie podana wartość, wtedy zostanie wybrana.

Informacje o zmianie daty są dostarczane z IANA Time Zone DataBase. Nowsza wersja jest dostarczana z updatem jdk ale może być podmieniona ręczniie (jdk/jre/lib/tzdb.dat).

Przy tworzeniu ZoneDateTime na podstawie Instant nie ma niejednoznaczności.

Instant

Przechowuje ilość sekund (long) i nanosekund (int) od początku ery (1970-01-01T00:00:00Z). Ma metody plusSeconds(), plusMillis() i plusNanos() i jedną która przyjmuje ilość i jednostkę. Czas jest zapisany w strefie GMT – nie występują komplikacje związane ze zmianą czasu – przesunięcie jest stałe (zerowe). Stosowany np. jako timestamp.

TemporalAdjuster

Można stosować poprzez wywołanie metody adjustInto(temporal) na obiekcie TemporalAdjuster lub wywołując
with(thisAdjuster) na obiekcie Temporal (wywołanie jest delegowane do Adjustera).

Domyslne Adjustery umieszczone w TemporalAdjusters

  • firstDayOfMonth
  • lastDayOfMonth
  • firstDayOfNextMonth
  • lastDayOfYear
  • firstDayOfNextYear
  • firstInMonth(Dzien tygodnia)
  • lastInMonth(DzienTygodnia)
  • next(DzienTygodnia)
  • previous(DzienTygodnia)
  • previousOrSame(DzienTygodnia)
  • lastInMonth(DzienTygodnia)
  • dayOfWeekInMonth (licznik, DzienTygodnia)

Licznik w ostatniej opcji oznacza które to ma być wystąpienie dnia tygodnia w miesiącu. Może wyjść poza zakres miesiąca do następnego: np 8 wtorek stycznia da luty;
-1 to ostatnie wystąpienie tego dnia tygodnia w tym miesiącu, -2 to przedostatnie itd (można cofnąć się do poprzedniego miesiąca)
0 to ostatnie wystąpienie w poprzednim miesiącu

Period

Zawiera pola int: year, month, day. Pola mogą mieć różne znaki. Dodanie Period.of(0, -1, 16) odejmie miesiąc i doda 16 dni. Nie można dodawać go do LocalTime. Bierzę pod uwagę zmiany czasu przy dodawaniu go do ZonedDateTime.
Nie można chainować Period.of (to statyczna metoda).
Ma metody plusYears(), minusDays() itd. które już są instancyjne.
Period domyślnie nie jest normalizowany tzn 24 miesiące nie są zamieniane na 2 lata. Służy do tego metoda normalized().

Inne przydatne metody:
toTotalMonths(), multipliedBy(), negated(), between()
Period toString() wyświetla PxYyMzD ale parse() przyjmuje tez W – tygodnie i może mieć minus przy dowolnej sekcji.

Duration

Zawiera pola long seconds i int nanos.
Ma metody analogiczne do Period oraz pomocniczne:plusDays(), plusHours(). Pobranie sekund i nanosekund ma przedrostek get…(), pozostałe jednostki mają metodę to…().
Ujemne nanosekundy są odejmowane od pełnych sekund. Sekundy mają metody związane z ich znakiem isNegative(), isNegative(), abs(). Dodawanie Duration nigdy nie bierze pod uwagę zmiany czasu.
toString() wyświetla PTsekundy.nanoSekundyS
parse() może przyjmować niecałkowite sekundy

Clock

Clock pozwala zastapic czas sytemowy.
Domyślne implementacje to:

  • fixed – cały czas zwraca jedną wartość
  • offset – opakowuje inny zegar i dodaje/odejmuje od niego stała wartość
  • tick – zaokrągla o wskazanie w Duration
  • tickSeconds i tickMinutestick w którym Duration to seconda i minuta

Zegar zawiera informację o strefie czasowej.

Inne klasy

MonthDay – ma metodę isValidYear(year) która dotyczy tylko 29 lutego, czyli pokrętnie sprawdza czy rok jest przestępny
YearMonth – ma metodę do tego samego nazwaną isLeapYear() i drugą do sprawdzania czy dzień istnieje w miesiącu – isValidDay(day)
OffsetDateTime – ZonedDateTime pozbawione reguł zmiany czasu, składa się z OffsetTime i LocalDateTime.

ChronoLocalDate

Interfejs do przechowywania daty w dowolnym kalendarzu. Zalecanie jest nie używanie go, posługiwanie się LocalDate a tłumaczenie do innych kalendarzy traktować jako lokalizacje. Pisanie aplikacji posługującej się abstrakcyjnym kalendarzem będzie podatne na błędy przez nieświadome stosowanie założeń dotyczących kalendarza gregoriańskiego: stała ilość miesięcy w roku, podobna długość miesięcy, rozpoczynanie miesiąca od pierwszego dnia, mniejszy numer roku oznaczający wcześniejszy rok.

Data Juliańska

Przy stosowaniu kalendarza ciężko obliczyć liczbę dni między dwoma datami. Dodatkowo jeśli jedna z dat jest p.n.e a druga n.e. od wyniku trzeba odjąć 1 ponieważ w tym aktualnym sposobie zapisu dat nie występuje rok 0. Tzn. po 1 p.n.e występuje rok 1 n.e. W czasie powstawania koncepcji „naszej ery” nie istniała koncepcja zera.
Powoduje to problemy z liczeniem dlatego powstała data Juliańska (JD – nie mylić z kalendarzem Juliańskim). Zakłada wybranie roku zerowego (konkretnie 4713 p.n.e) a następnie zapisywanie dat jako ilości dni które minęły od tego czasu (w oryginalnym formacie dzień zaczynał się w południe). Godziny są zapisywane jako części ułamkowe dnia. Data stosowana np. w astronomii, pomysł został zaadoptowany przez programistów Unixa (z uwzględnieniem sekundy przestępnej). Stosowana jest zmodyfikowana data Juliańska (MJD) która przesuwa początek epoki 2400000,5 dni do przodu, żeby operować na mniejszych liczbach. Połówka na końcu odzwierciedla, że w MJD dzień zaczyna się o północy. Java ma implementacje obu dat w klasie JulianFields ale nie wspiera części ułamkowych – godzin.

Pomiar czasu a bezpieczeństwo

Pomiar odchyleń od prawidłowego czasu umożliwia przeprowadzanie ataków deanonimizujących. Zwiększenie obciążenia anonimowego serwera wpływa na jego temperaturę, temperatura wpływa na działanie oscylatora co tworzy odchylenia, sprawdzając czas „podejrzanych” serwerów można wnioskować który z nich działa również w anonimowej sieci.

Pomiar czasu przetwarzania danych może, w niektórych przypadkach, zdradzać informacje na temat samego przetwarzania.
Przykładowo: aplikacja porównując hasła (zamiast hashy) po jednym znaku (zamiast całych) zwróci wynik po czasie proporcjonalnym do liczby prawidłowo wpisanych znaków.

Źródła:
Dokumentacja pakietu java.time
www.timeanddate.com UTC
www.timeanddate.com Sekunda przestępna
Internet Draft UTC-SLS
Hot or Not: Revealing Hidden Services by their Clock Skew Steven J. Murdoch

JLS Finalizacja i rodzaje referencji

Finalizacja

Służy do zwalniania zasobów zainicjalizowanych poza JVMem. Realizuje to samo co sekcja finally lub try with resources ale dla obiektów o widoczności większej niż zasięg metody. Umożliwia umieszczenie kodu zwalniającego zasoby wewnątrz obiektu którego dotyczy, co utrudnia niepoprawne wykorzystanie.

Klasa Object ma metodę protected void finalize() throws Throwable { }. Obiekty mogą nadpisywać tą metodę. Kontrakt tej metody mówi, że będzie wywołana przez Garbage Collectora przed ponownym użyciem pamięci zajmowanej przez obiekt (co może nie nastąpić). Czas jej wykonania wpływa na czas działania GC.
„Finalizer” może pojawiać się w dwóch kontekstach: jako implementacja metody finalize, lub jako obiekt rozszerzający FinalReference (patrz niżej) który przechowuje referencje do obiektu przeznaczonego do finalizacji.
Enumy nie mogą mieć finalizerów.

Mechanizm finalizacji jest w większości napisany w Javie. Wyjątek to pole zawierające elementy które mogą być kiedyś finalizowane (unfinalized) uzupełniane przez JVMa oraz metody natywne (według komentarzy w kodzie) pośredniczące w wywołaniu metod opisanych poniżej. Pośredniczące metody natywne równie dobrze mogą same zaimplementować te metody.

Metody System.runFinalization(), System.gc() w teorii mogą przyśpieszyć wywołanie finalizerów.
JVM nie czeka na skończenie pracy finalizerów kiedy się wyłącza. Wyjątek to zastosowanie System.runFinalizersOnExit(true) metoda jest niezalecana i ma dwie wady: finalizacja może być przeprowadzona na „żywym” obiekcie co może skutkować niespójnymi danymi oraz nie zapewnia, że finalizer zakończy pracę w sytuacji kiedy jego wykonanie rozpocznie się przed wyłączaniem JVM.
Po załadowaniu klasy Finalizer.java uruchamiany jest wątek który kolejno wykonuje finalizację obiektów dodanych do kolejki przez JVMa.

Model pamięci gwarantuje, że metoda będzie wykonywana po wszystkich zapisach a wszystkie odczyty nie będą widziały jej skutków. Finalizacja może przebiegać wielowątkowo. Aktualna javowa implementacja obejmuje dwa wątki dla finalizacji i jeden do obsługi referencji (patrz niżej). Główny obsługujący finalizacje ma priorytet o 2 mniejszy od maksymalnego. Drugi jest uruchamiany jako wątek systemowy i wykorzystywany (według dokumentacji) tylko dla runFinalization i runFinalizersOnExit. W przeciwieństwie do pierwszego nie wchodzi w stan uśpienia.
Finalizer może być wywołany tylko raz dla konkretnego obiektu (implementacja tego jest raczej po stronie JVMa).

W przeciwieństwie do konstruktora finalizer nie wywołuje swojego odpowiednika w nadklasie – trzeba to zrobić jawnie.
Jeśli klasa może być rozszerzana wykonanie finalizacji dla instancji klas rozszerzających można wymusić można przez utworzenie klasy wewnętrznej („strażnika”) zawierającego pożądaną implementacje finalize, a następnie przechowywania referencji do instancji tej klasy wewnątrz obiektu który może być rozszerzany. Czas życia „strażnika” będzie pokrywał się z czasem życia instancji klas rozszerzających.

Podział obiektów względem dostępności

  • reachable wykorzystywany przez aplikację
  • finalizer-reachable możliwy do uzyskania tylko przez obiekt przeznaczony do finalizacji
  • unreachable nieosiągalny

Optymalizacje kompilatora mogą sprawić, że obiekt będzie możliwy do finalizacji wcześniej niż to wynika z kodu. Dotyczy tylko referencji trzymanych na stosie (wewnątrz wywołań metody). Możliwa jest finalizacja obiektu na rzecz którego jeszcze wywoływane są metody o ile stan tego obiektu nie jest później odczytywany.

Finalizer umożliwa ponowne „ożywienie” obiektu (np. przez dodanie referencji do niego do listy z użyciem this). Taki obiekt tworzy wyciek pamięci bo GC traktuje go jako usunięty (uruchomiono jego finalizer) mimo, że do samego usunięcia nie doszło.

Jeśli podczas finalizacji będzie rzucony wyjątek to jest on ignorowany a finalizacja tego obiektu się kończy.


Typy referencji

Typy rozróżniane z punktu widzenia Garbage Collectora, nie w kategoriach obiektu na który wskazują (instancja klasy, interfejsu, tablicy lub nulla). GC bierze pod uwagę „najsilniejszy” typ referencji po którym można dotrzeć do obiektu. Wszystkie typy poza pierwszym to wrappery na silną referencje.

Dodatkowe typy referencji są umieszczone w pakiecie java.lang.ref. Umożliwiają rozdzielenie czasu życia obiektu od czasu życia referencji do niego, które domyślnie są jednakowe. Częsty przypadek wykorzystania to obiekty z metadanymi dotyczącymi innych obiektów. Metadane nie zawsze mogą być umieszczone wewnątrz obiektów które opisują. Jeśli zastosowano mapę do powiązania opisujących i opisywanych obiektów lub opisujące obiekty zawierają referencje do opisywanych wtedy istnieją trzy opcje:

  • czas życia opisywanych obiektów jest rozszerzany do czasu życia metadanych
  • czas życia opisywanych obiektów może być skracany przez jawne nullowanie referencji (czyli „ręczne” zarządzanie czasem życia obiektu)
  • wykorzystanie, poza zwykłymi, także referencji nie wpływających na działanie GC.

strong

Standardowe referencje, możliwość dotarcia do obiektu (zaczynając w GC roots) przez tą referencję uniemożliwia usunięcie go przez GC.

soft

Referencja gwarantuje usunięcie obiektu zanim JVM rzuci OutOfMemoryError. Nie ma innych gwarancji co do czasu życia obiektu: może zostać usunięty od razu po utworzeniu lub nigdy (jeśli nie brakuje pamięci). Implementacje JVMa są zachęcane do nieusuwania obiektów które były niedawno wykorzystywane. Przeważnie wykorzystywane przy implementowaniu cache’a.

weak

Istnienie odwołania przez słabą referencje nie wpływa na czas życia obiektu. Stosowane np. do implementacji mapowań standaryzująych. Przykładem takiego mapowania jest cache Integera czy BigDecimala (mimo że oba używają silnych referencji – mapowane obiekty są małe). Mapa agreguje wszystkie obiekty ale nie zapobiega ich usunięciu. Dopiero użytkownik mapy tworzy silną referencje do obiektu na którym pracuje uniemożliwiając jego usunięcie.

phantom

Jedyny typ referencji który nie umożliwia „dotarcia” do wskazywanego elementu. Używany, w połączeniu z kolejką referencji, do informowania o tym, że obiekt może zostać usunięty (finalizacja już miała miejsce). Analogicznie działanie umożliwia finalizer obiektu ale użycie tego typu referencji jest bezpieczniejsze, przez brak możliwości „wskrzeszenia” obiektu. Użycie phantom reference umożliwia kontrolowanie który wątek odpowiada za finalizacje, w przypadku finallize aplikacja nie ma na to wpływu. W przeciwieństwie do finallize kod zwalniający zasoby jest umieszczony poza obiektem którego dotyczy.

final

Wykorzystywany przez finalizery. GC pośrednio tworzy ich instancje.


Reference Queue

Referencje inne niż silne można powiązać z kolejką. Kolejka dostarcza informację, o tym że zmieniła się dostępność referenta. Obiekty referencji Soft i Weak trafiają do kolejki w momencie w którym wskazywane obiekty przestają być możliwe do otrzymania przez te typy referencji (czyli w momencie opuszczania stanu Soft Reachable i Weak Reachable). Odwrotnie dla Phantom Reference: obiekty trafiają do kolejki w momencie w którym wchodzą w stan Phantom Reachable. Final Reference są tworzone przez GC kiedy obiekt nadaje się do finalizacji.
Obiekty referencji same podlegają usuwaniu przez GC. Jeśli wskazywany obiekt został usunięty wrapper zwraca nulla.

Jeśli referencje Soft i Weak zostaną usunięte przez GC wtedy automatycznie trafiają do kolejki o ile są do jakiejś przypisane.
Obiekt do którego istnieje Phantom Reference nie zostanie usunięty do czasu aż jego wszystkie fantomowe referencje nie zostaną usunięte lub znullowane przez metodę clear().

Reference Handler

Po załadowaniu klasy dziedziczącej po Reference tworzony jest oddzielny wątek daemona ReferenceHandler, z najwyższym priorytetem, którego zadanie to dodawanie referencji do kolejek. Referencje są dodawane do statycznego pola transient private Reference discovered; w klasie Reference przez Garbage Collectora, pole jest synchronizowane i umożliwia połączenie obiektów referencji w listę. GC rozszerza tę listę a Reference Handler ją skraca.

WeakHashMap

Implementacja mapy która wykorzystuje słabe referencje do przechowywania kluczy. W momencie usunięcia klucza jest on dodawany do wewnętrznej kolejki referencji. Następna interakcja z mapą usunie wpis który zawierał usunięty klucz. Trzeba upewnić się, że wartość nie ma referencji do swojego klucza (zapobiegłoby to jego usunięciu). Wartości mogą być ręcznie opakowane przez słabą referencję przed umieszczeniem w mapie, ale w przypadku ich finalizacji, odpowiadające im wpisy nie będą automatycznie usuwane.


Cleaner

Jeśli kod finalizujący jest prosty może być umieszczony w Cleanerze. Cleaner jest elementem dwukierunkowej listy, zawiera referenta (jako fantomową referencję), referencje do następnego i poprzedniego Cleanera oraz kod wywoływany w czasie sprzątania (w formie Runnable). Sprzątanie jest wykonywane dla wszystkich elementów listy. Kod sprzątający jest wykonywany przez wątek ReferenceHandler a nie wątek/wątki finalizujące. Istnienie Cleaner nie wyłącza finalizera. Cleaner znajduje się w paczce sun.misc co sugeruje niewykorzystywanie go poza core’owym kodem javy. Jest wykorzystywany w pakiecie NIO

Java 9 wprowadziła nowszą wersje Cleanera umieszczoną w java.lang.ref razem powyższymi typami referencji.

Źródła:
„The Java® Language Specification Java SE 8 Edition” James Gosling, Bill Joy, Guy Steele, Gilad Bracha, Alex Buckley
„Inside the Java Virtual Machine” Bill Venners
„Java. Efektywne programowanie” Wydanie II Joshua Bloch
Plugging memory leaks with soft references Brian Goetz
Plugging memory leaks with weak references Brian Goetz
Dokumentacja Soft Reference
Dokumentacja Weak Reference
Dokumentacja Phantom Reference
Dokumentacja Reference – klasy bazowej dla powyższych
Dokumentacja Reference Queue
Dokumentacja WeakHashMap
Kod Cleanera

JLS Unicode

Translacja Unicode

    Kolejność operacji:

  • Zamiana ucieczek Unicode na znaki Unicode
  • Wyszczególnienie z powyższych znaków końca linii
  • Usunięcie białych znaków i komentarzy

Powyższe kroki są wykonywane przed analizą składniową w kompilacji. Można wykorzystywać ucieczki Unicode w formacie \uxxxx gdzie xxxx to wartość znaku zapisana szesnastkowo. Ma to na celu danie możliwości zapisania w programie znaków Unicode przy wykorzystywaniu tylko znaków ASCII.

\u0070\u0075\u0062\u006C\u0069\u0063\u0020\u0063\u006C\u0061\u0073\u0073\u0020\u004F\u006A\u0061\u0063\u0069\u0065\u007B\u0070\u0075\u0062\u006C\u0069\u0063\u0020\u0073\u0074\u0061\u0074\u0069\u0063\u0020\u0076\u006F\u0069\u0064\u0020\u006D\u0061\u0069\u006E\u0028\u0053\u0074\u0072\u0069\u006E\u0067\u005B\u005D\u0020\u0061\u0072\u0067\u0073\u0029\u007B\u0053\u0079\u0073\u0074\u0065\u006D\u002E\u006F\u0075\u0074\u002E\u0070\u0072\u0069\u006E\u0074\u006C\u006E\u0028\u0022\u0048\u0065\u006A\u0068\u006F\u0022\u0029\u003B\u007D\u007D zapisane w pliku Ojacie.java można skompilować i uruchomić (raczej z konsoli).

Ucieczki Unicode muszą mieć nieparzystą ilość backslashy: \u0023 to #, \\u0023 to \u0023, \\\u0023 to \# itd …

Ucieczki mogą mieć więcej niż jedno u. Ostatnie musi poprzedzać cztery znaki szesnastkowe inaczej występuje błąd kompilacji (prekompilacji) \uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu0023 jest akceptowalne.

Zamiana ucieczek na znaki odbywa się tylko raz: \u005cu0023 nie da w wyniku # (005c to kod \).

Specyfikacja dopuszcza konwersję kodu z Unicode do ASCII. Znaki niemieszczące się w ASCII są tłumaczone na ucieczki, ucieczki mają dodatkowe u np. \uu0023.


Znaki końca linii i pliku

Dopuszczalne znaki końca linii to:

  • CR (\u000d) wykorzystywany w : Commodore, Apple II, Mac OS (do wersji 9), Microware OS-9
  • LF (\u000a) wykorzystywany w: Unix, BeOS, AmigaOS, MorphOS, RISC OS, GNU/Linux, Mac OS, Multics
  • CRLF (\u000d\u000a) wykorzystywane w: DOS, OS/2, Microsoft Windows, Symbian, DEC RT-11.

CRCRLF stworzy tylko jeden znak końca linii. Kompilator może ustalać numery linii na podstawie znaków końca linii. Komentarz rozpoczynający się od // kończy się na znaku końca linii.

Jeśli plik kończy się znakiem SUB (\u001a) przeważnie wysyłanym przez control + Z, to jest on ignorowany.
Jego pełna nazwa to Substitute character. Przy wysłaniu znaku do linuksowego terminala shell wysyła do procesu sygnał SIGSTOP który pauzuje jego wykonywanie. Jest też zwyczajowo używany do oznaczania końca pliku (mimo tego że mogą po nim występować jakieś dane – przeważnie nie będą wyświetlone).

Znak jest także wykorzystywany jeśli nie ma możliwości konwersji znaku z jednej reprezentacji do innej. Pominięcie takiego znaku mogłoby skutkować lukami bezpieczeństwa.
Jeśli moduł dodawania użytkowników korzysta z innego kodowania niż moduł autoryzujący a nieznane znaki są pomijane wtedy jest możliwość utworzenia konta użytkownika o nazwie składającej się z już istniejącego użytkownika z uprawnieniami + znaki nieznane przez system kodowania. Moduł dodawania użytkowników widziałby dwóch różnych użytkowników a moduł uwierzytelniania jednego.


Komentarze i białe znaki

Zawartość komentarzy i białe znaki mogą mieć wpływ na kompilacje.
// char lf = '\u000a'; nie skompiluje się – ucieczka zostanie zmieniona na znak końca linii. Inicjalizacja zmiennej typu char mieć miejsce w jednej linii (dla Stringa nawiasy także muszą być zamknięte w tej samej linii ale można użyć znaku plusa i utworzyć nowy literał w następnej linii który będzie dodany do poprzedniego).
Nie dotyczy to komentarza utworzonego przez /* */ lub umieszczenia \n w komentarzu, które jest przetwarzane w czasie kompilacji nie przed.

Białe znaki mają znaczenie jeśli tokeny (najmniejsze fragmenty składniowe) które rozdzielają tworzą inny token gdy są nierozdzielone. += to operator przypisania ale + = daje błąd kompilacji.


Różne

Identyfikatory są takie same jeśli są reprezentowane za pomocą tych samych znaków Unicode. Czyli mogą być wyświetlane w ten sam sposób a dotyczyć różnych zmiennych.

Literał 2147483648 może wystąpić tylko z minusem inaczej ma miejsce błąd kompilacji. Integer w którym domyślnie są zapisywane literały całkowite ma zakres ujemny o jeden większy od zakresu dodatniego przez zastosowanie zapisu U2. Analogicznie dla longa i wartości 9223372036854775808.
Double i Float także mają minimalne (względem odległości od 0) i maksymalne wartości. Jeśli reprezentacja niezerowej wartości jest reprezentowana jako zero – występuje błąd kompilacji, tak samo jeśli wartością jest nieskończoność.
Przy operacjach w których występują bardzo duże i bardzo małe liczby zmiennoprzecinkowe ich reprezentacje muszą być „sprowadzone do wspólnego mianownika” a konkretnie do wspólnej mantysy co skutkuje utratą informacji na temat małej liczby.

Źródła:
„The Java® Language Specification Java SE 8 Edition” James Gosling, Bill Joy, Guy Steele, Gilad Bracha, Alex Buckley
Wikipedia znaki końca linii
Wikipedia Substitute character
Zalecenia Unicode dt. reprezentowania niemapowalnych znaków
Wikipedia Liczby zmiennoprzecinkowe w informatyce

OCA Wyjątki

Wyjątki

Stacktrace wyjątku jest zapisywany w momencie tworzenia wyjątku. Jeśli wyjątek jest utworzony i przekazany do innej metody która go rzuca wtedy wyświetlony stacktrace będzie odpowiadał miejscu w którym był tworzony nie rzucony.

toString() wyświetla nazwę wyjątku i jego tekst
getMessage() zwraca wiadomość
stacktrace jest wyświetlany przez printStackTrace().

Tylko ostatni z rzucanych wyjątków jest propagowany dalej. Rzucanie wyjątku w catch i kolejnego w finally skutkuje rzuceniem drugiego. W tej sytuacji jeśli to sprawdzany wyjątek to musi być zadeklarowany w sygnaturze.

throws w nazwie metody może być „na zapas” ale jeśli jest catch sprawdzanego wyjątku wyjątek musi być możliwy do rzucenia w try.
Jeśli wyjątek to Exception to powyższa zasada nie obowiązuje. Wtedy Exception jest traktowany jako RuntimeException bo może nim być.

Jeśli wywoływana jest nadpisana metoda której bazowa metoda rzuca wyjątek a nadpisująca nie wtedy referencja ma znaczenie – referencja do bazowej wymaga try catch; referencja do rozszerzającej nie.

Można łapać i deklarować RuntimeException.
Można łapać i deklarować Errory.

Źródła:
„OCA: Oracle Certified Associate Java SE 8 Programmer I Study Guide: Exam 1Z0-808” Jeanne Boyarsky, Scott Selikoff
„JA+ V8 for Oracle Certified Associate – Java SE8 Programmer I” http://enthuware.com

OCA Projektowanie klas

Dziedziczenie

Wywołanie innego konstruktora (this lub super) musi być pierwszą instrukcją konstruktora.

Jeśli konstruktor klasy nadrzędnej nie jest wywoływany jawnie wtedy kompilator dodaje odwołanie do bezparametrowego konstruktora klasy nadrzędnej. Jeśli klasa nadrzędna nie ma konstruktora bezparametrowego wtedy jej konstruktor parametrowy musi być wywołany przez konstruktor klasy dziedziczącej.

Konstruktor klasy nadrzędnej nie ma dostępu do metod klasy rozszerzającej (nic o niej nie wie) ale jeśli wywoła swoją metodę która jest nadpisana w klasie rozszerzającej to zostanie wywołana nadpisująca metoda. Jeśli metoda korzysta z pól, pola te będą miały wartość domyślną nawet jeśli jest ona zmieniana w konstruktorze klasy rozszerzającej (konstruktor ten będzie wywołany dopiero po utworzeniu klasy nadrzędnej).

Klasy abstrakcyjne

Klasa abstrakcyjna nie musi mieć żadnych abstrakcyjnych metod.

Metoda abstrakcyjna nie może mieć implementacji ani być prywatna.

Implementacja abstrakcyjnych metod podlega tym samym zasadom co nadpisywanie.

Nadpisywanie

Żeby nadpisanie miało miejsce muszą być spełnione warunki:

  • sygnatury metod muszą być takie same
  • metoda nadpisująca musi być co najmniej tak samo dostępna
  • metoda nadpisująca nie może rzucać ogólniejszego lub nowego sprawdzanego wyjątku (może precyzować)
  • jeśli metoda nadpisująca coś zwraca, typ zwracany musi być ten sam lub bardziej szczegółowy (kowariantnynie dotyczy typów prostych)

Prywatne metody nie są nadpisywane ale mogą być deklarowane metody o tej samej sygnaturze które nie mają związku z metodą klasy bazowej.

Przy nadpisywaniu referencja nie ma znaczenia, istotny jest tylko rzeczywisty typ obiektu (który jest sprawdzany w czasie uruchomienia).

Przesłanianie

Metody statyczne mogą być przesłaniane (poza finalnymi). Warunki dla przesłaniania są takie same jak dla nadpisywania oraz metoda przesłaniana oraz przesłaniająca muszą być statyczne (inaczej występuje błąd kompilacji).

Zmienne statyczne mogą być przesłaniane przez niestatyczne i odwrotnie.

Przesłoniętą metodę można wywołać przez super.metoda() lub rzutowanie, analogicznie dla zmiennych.

Przy przesłanianiu referencja decyduje która metoda będzie wywołana (dzieje się to w czasie kompilacji).

Przeciążanie

Nie można przeciążać po typie zwracanym.


Interfejsy

Interfejsy domyślnie mają modyfikator abstract, dozwolony dostęp publiczny lub pakietowy.

Pola domyślnie mają modyfikatory public static final.

Do pól można odwołać się przez nazwę interfejsu, nazwę klasy która go implementuje lub referencje do niej a wewnątrz klasy która go implementuje także bez żadnej referencji (przez this).

Metody domyślnie mają modyfikatory public abstract.

Metody interfejsu tak jak metody abstrakcyjny nie mogą być private, protected ani final.

Jeśli wiele implementowanych interfejsów ma metodę o tej samej sygnaturze to wystarczy ją zaimplementować raz.

Jeśli metody w dwóch interfejsach mają te same sygnatury ale różne typy zwracane – nie mogą być implementowane jednocześnie.

Metody domyślne interfejsów nie mogą być statyczne, ale można mieć metodę domyślną w interfejsie dziedziczącym o takiej samej sygnaturze jak metoda statyczna w metodzie bazowej (odwrotnie jest błąd kompilacji).

SuperInterface.super.method() wywołuje metodę domyślną z rozszerzanego interfejsu, można przejść tylko jeden poziom do góry.
Samo super się nie skompiluje ponieważ interfejs może rozszerzać kilka interfejsów.

Można nadpisywać metody domyślne (rozszerzając interfejs z metodą domyślną).

Metody domyślne umożliwiają wielokrotne dziedziczenie. Jeśli nie jest jasne która metoda powinna być wywołana występuje błąd kompilacji, chyba że klasa sama nadpisuje metodę która jest dziedziczona domyślnie z kilku interfejsów.

Interfejsy mogą mieć metody statyczne ale te metody nie są dziedziczone przez klasy implementujące interfejs (nie można się do nich odwołać też przez referencje do interfejsu). Dzięki temu nie ma wielokrotnego dziedziczenia (konieczność jawnego napisania o metodę z którego interfejsu chodzi).

Jeśli interfejs ma metodę która także jest w klasie rozszerzanej ale o mniejszym dostępie. Klasa która implementuje i rozszerza jednocześnie musi przesłonić metodę żeby zwiększyć jej poziom dostępu.

Istnienie pól lub metod o jednakowych sygnaturach w interfejsach implementowanych przez jedną klasę nie generuje błędu. Dopiero niejednoznaczne odwołanie (bez podania konkretnego interfejsu) generuje błąd kompilacji.

Źródła:
„OCA: Oracle Certified Associate Java SE 8 Programmer I Study Guide: Exam 1Z0-808” Jeanne Boyarsky, Scott Selikoff
„JA+ V8 for Oracle Certified Associate – Java SE8 Programmer I” http://enthuware.com

OCA Metody i enkapsulacja

Struktura metody

dostęp modyfikatory typZwracany nazwa parametry
pierwsze dwa nie są obowiązkowe.

Opcjonalne modyfikatory:
static, abstract, final, synchronized, native, strictfp mogą występować w dowolnej kolejności, konstruktor nie może mieć żadnego z nich.

static oraz final są dozwolone dla pól i metod
transient oraz volatile – tylko dla pól
synchronized, abstract oraz native – tylko dla metod
final – dozwolone dla zmiennych lokalnych.

Klasy finalne nie mogą być rozszerzane a metody nadpisywane (ale mogą być przeciążane).
Zmiennie finalne nie mogą zmienić wartości ale mogą być przesłonięte.

Statyczna metoda finalna nie może być przesłaniana.

Może istnieć metoda i pole o tej samej nazwie w tym samym zasięgu.


Wywołanie metody

Przy dopasowaniu metody do wywołania java używa metody z najdokładniejszymi typami parametrów, nigdy typy zmiennych nie są zawężane (w przeciwieństwie do przypisywania zmiennych całkowitych). Dotyczy także konstruktorów.

Priorytety przy dopasowaniach:

  • dokładne dopasowanie
  • bardziej pojemny typ
  • automatyczne opakowywanie
  • varargs

Przeprowadzana jest tylko jedna konwersja.
Nie będzie konwersji na bardziej pojemny typ prosty a następnie jego odpowiednik obiektowy.

Wywołanie metody protected na rzecz obiektu z innego pakietu jest możliwe tylko jeśli posługujemy się referencją podklasy.
W przypadku posługiwania się referencją w innym pakiecie to musi być referencja typu dziedziczącego.

Modyfikator private dotyczy wszystkich obiektów tego typu a nie tylko jednego obiektu (patrz equals()).

Do metod i pól statycznych można odwoływać się przez referencję instancji (nawet jeśli wskazuje na null).

Przy wywoływaniu metody z nullem wywoływana jest najbardziej szczegółowa implementacja (klasa która jest najniżej w hierarchii dziedziczenia).

Odwoływanie do pól lub metod statycznych klasy nadrzędnej jest możliwe przez referencje klasy dziedziczącej (ale klasa dziedzicząca nie jest wtedy ładowana).

Metoda statyczna nie ma dostępu do metod i pól instancyjnych.

Finalna zmienna musi być zainicjalizowany w konstruktorze lub w inicjalizacji instancyjnej, ewentualnie w sekcji statycznej jeśli jest statyczny.

Finalne zmienne typów prostych są konwertowane do literałów (są znane w czasie kompilacji).

Typ podawany w lamdzie musi być identyczny z typem w sygnaturze interfejsu funkcyjnego. Jeśli w interfejsie jest List wtedy ArrayList w lambdzie się nie skompiluje.

Źródła:
„OCA: Oracle Certified Associate Java SE 8 Programmer I Study Guide: Exam 1Z0-808” Jeanne Boyarsky, Scott Selikoff
„JA+ V8 for Oracle Certified Associate – Java SE8 Programmer I” http://enthuware.com