Vela → inteligentny cache dla serii czasowych i nie tylko

W fintechu często musimy przetwarzać dość masywne ilości danych dotyczących kursów walut. Otrzymujemy dane z różnych źródeł, a każde z nich ma swoje własne wyobrażenie o tym, jak ekstrapolować wartości kursów na jutro, pojutrze, następny miesiąc, a nawet na kolejne trzy lata. Gdyby ktoś potrafił przewidywać kursy, prawidłowomożna by zamknąć interes i po prostu zamieniać pieniądze tam i z powrotem. Niektóre źródła cieszą się większym zaufaniem, inne dostarczają same śmieci, z rzadkimi wstawkami niemal poprawnych wartości, ale za to dla egzotycznych par. Nasza praca polega na tym, aby przesiać te dziesiątki tysięcy wartości na sekundę i określić, co dokładnie pokazać klientom. Musimy odfiltrować jedną właściwą wartość z tony brudu i mułu, jak robią to flamingi podczas posiłku.

Vela → inteligentny cache dla serii czasowych i nie tylko

Szczególną cechą flamingów jest masywny, wygięty w dół dziób, za pomocą którego filtrują pokarm z wody lub mułu.
 — Wiki

Tak narodziła się biblioteka Vela, która przechowuje cache stanu dla wielu wartości w zadanych przedziałach czasowych. Pod maską na bieżąco odrzuca złe i przestarzałe dane oraz zapewnia dostęp do ostatnich N zweryfikowanych wartości dla każdego klucza (pary walut, w naszym przypadku).

Załóżmy, że zbieramy kursy dla trzech par walut. Najprostsza definicja Vela dla przechowywania aktualnego stanu będzie wyglądać mniej więcej tak:

defmodule Pairs do
  use Vela,
    eurusd: [sorter: &Kernel. 0
end

Aktualizacja wartości

Vela.put/3 funkcja kolejno wykona następujące:

  • wywoła validator na wartości, jeśli taki jest zdefiniowany (patrz rozdział Walidacja poniżej);
  • doda wartość albo do szeregu dobrych wartości, jeśli walidacja zakończyła się pomyślnie, albo do szeregów pomocniczych :__errors__ w przeciwnym razie;
  • wywoła sortowanie, jeśli sorter jest zdefiniowany dla tego klucza, lub po prostu umieści wartość na początku listy (LIFO, patrz rozdział Sortowanie poniżej);
  • obetnie szereg zgodnie z parametrem :limit przekazanym podczas tworzenia;
  • zwróci zaktualizowaną strukturę Vela.

iex|1 > pary = %Pairs{}
iex|2 > Vela.put(pary, :eurcad, 1.0)
#⇒ %Pairs{..., eurcad: [1.0], ...}
iex|3 > Vela.put(pary, :eurcad, -1.0)
#⇒ %Pairs{__errors__: [eurcad: -1.0], ...}
iex|4 > pary |> Vela.put(:eurusd, 2.0) |> Vela.put(:eurusd, 1.0)
#⇒ %Pairs{... eurusd: [1.0, 2.0]}

Również Vela implementuje Dostęp, więc można skorzystać z dowolnej z standardowych funkcji do głębokiej aktualizacji struktur z arsenału Kernel: Kernel.get_in/2, Kernel.put_in/3, Kernel.update_in/3, Kernel.pop_in/2, i Kernel.get_and_update_in/3.

Walidacja

Walidator może być zdefiniowany jako:

  • zewnętrzna funkcja z jednym argumentem (&MyMod.my_fun/1), otrzyma tylko wartość do walidacji;
  • zewnętrzna funkcja z dwoma argumentami, &MyMod.my_fun/2, otrzyma parę serie, wartość do walidacji;
  • moduł implementujący Vela.Validator;
  • parametr konfiguracyjny threshold, i – opcjonalnie – compare_by, patrz rozdział Porównanie poniżej.

Jeśli walidacja przebiegnie pomyślnie, wartość zostanie dodana do listy pod odpowiednim kluczem, w przeciwnym razie krotka {serie, wartość} zostanie wysłana do :__errors_.

Porównanie

Wartości przechowywane w tych seriach mogą być dowolne. Aby nauczyć Vela je porównywać, należy przekazać compare_by parametr w definicji serii (chyba że wartości mogą być porównywane standardowym Kernel.</2); parametr ten powinien mieć typ (Vela.value() -> number()). Domyślnie to po prostu & &1.

Ponadto, w definicji serii można przekazać parametr comparator do obliczania wartości delta (min/max); na przykład, przekazując Date.diff/2 jako komparator, można uzyskać poprawne delty dla dat.

Innym wygodnym sposobem działania jest przekazywanie parametru threshold, który określa maksymalne dopuszczalne odniesienie nowej wartości do {min, max} przedziału. Ponieważ jest on określony w procentach, walidacja nie korzysta z comparator, ale nadal wykorzystuje compare_by. Na przykład, aby określić próg dla czasów dat, należy podać compare_by: &DateTime.to_unix/1 (do uzyskania wartości całkowitej) i threshold: 1, w wyniku czego nowe wartości będą dozwolone tylko wtedy, gdy mieszczą się w ±band przedziale od obecnych wartości.

Na koniec, można użyć Vela.equal?/2 do porównania dwóch pamięci podręcznych. Jeśli wartości definiują funkcję equal?/2 lub compare/2, to te funkcje zostaną użyte do porównania, w przeciwnym razie po prostu używamy ==/2.

Uzyskiwanie wartości

Przetwarzanie bieżącego stanu zwykle zaczyna się od wywołania Vela.purge/1, które usuwa przestarzałe wartości (jeśli validator jest uzależnione od timestamps). Następnie można wywołać Vela.slice/1, która zwraca keyword z nazwami wierszy jako klucze oraz pierwszymi, aktualnymi wartościami.

Można też skorzystać z get_in/2/pop_in/2 do niskopoziomowego dostępu do wartości w każdym wierszu.

Aplikacja

Vela może okazać się niezwykle przydatna jako pamięć podręczna szeregów czasowych w stanie procesu typu GenServer/Agent. Chcemy nigdy nie używać przestarzałych wartości kursów, dlatego po prostu utrzymujemy proces ze stanem, obsługiwanym Vela, z walidatorem pokazanym poniżej.

@impl Vela.Validator
def valid?(_key, %Rate{} = rate),
  do: Rate.age(rate) < @death_age

i Vela.purge/1 spokojnie usuwa wszystkie przestarzałe wartości za każdym razem, gdy potrzebujemy danych. Aby uzyskać aktualne wartości, po prostu wywołujemy Vela.slice/1, a kiedy potrzebna jest krótka historia kursu (cały szereg), po prostu zwracamy go – już posortowanego – z zwalidowanymi wartościami.

Sukcesów w pamięci podręcznej szeregów czasowych!

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster