Fieldy wykorzystujące cache

Okazało się, że są pewne błędy implementacyjne w fieldzie reverse-single-reference i fajnie by było ustalić rozwiązanie przed utworzeniem fielda cache-field. Po pierwsze nie aktualizujemy timestampa przy wpisywaniu wartości cache. Po drugie jeżeli serwer się wykrzaczy tuż po akcji delete, to timestamp nie zdąży się zaktualizować i przy uruchamianiu nie będziemy wiedzieli, że to pole wymaga aktualizacji (z racji, że obsługują to listenery typu post).

Zapraszam do komentowania i zgłaszania swoich pomysłów

Podejście naiwne

Przy starcie obliczamy na nowo wartości wszystkich pola będące cachem. Dla małych i średnich aplikacji to nie musi być takie złe, ale dla dużej bazy mam wątpliwości czy to dobry pomysł.

Podejście wykorzystujące transakcje

Mongo nie wspiera samo w sobie transakcji na wielu dokumentach, ale twórcy udostępniają dosyć obszerny dokument jak to zrobić: https://docs.mongodb.com/manual/tutorial/perform-two-phase-commits/. Nasz case powinien być dużo prostszy niż tu opisany, nie mniej będzie potrzebna dodatkowa kolekcja na transakcje o nazwie np. cache-updates. W momencie rozpoczęcia przetwarzania będziemy wstawiać rekord, po każdym kroku będziemy zmieniać stan (+ inne potrzebne operacje), a na koniec usuwać. Jeżeli w momencie startu aplikacji (to with_stopped_app będzie tu baaardzo pomocne do przetestowania) są jakieś rekordy w kolekcji, to uruchamiamy powiązane z nimi transakcje.

Pierwsze rozwiązanie na pewno nie wchodzi w grę… A z drugiej strony rozwiązanie z transakcjami to baaardzo duży refactor. Podejście z two-phase commit nie będzie konieczne, jeżeli przeskoczymy na wersję beta mongo, która umożliwia transakcje na wielu dokumentów :smiley:

Generalnie Sealious jest w wielu miejscach nieodporny na problemy związane z atomowością transakcji… Uważam, że najlepiej rozwiązać to w nowym tasku, przy użyciu mongowych transakcji. Wyobrażam sobie to tak, że nowa transakcja jest wywoływana za każdym razem, kiedy tworzymy nowy kontekst, i zamykana po jego zużyciu :slight_smile:

A cached field zróbmy wcześniej, bo i tak nie jesteśmy w innych miejscach odporni na atomość


Co do aktualizacji timestampa - uważam, że powinniśmy przechowywać informacje o ostatniej aktualizacji cache’u nie w last_modified_context.timestamp, ale w samym polu, tzn:

{
	body: {
		cached_field: {
			last_updated: 1522614683670,
			value: "cached value"
		}
	}
}

I oczywiście zwracalibyśmy w body zasobu tylko value, z pominięciem last_updated.

No właśnie też widziałem, że rzeczywiście gdzie indziej nie jesteśmy na to odporni - ok, seems legit. Zajmę się tym jutro.

1 Like

Implementacja jednak nie jest taka oczywista - już wiem co powoduje problem z Promise.map i to jest syndrom poważniejszego problemu. Kiedy napierdalamy requestami, to może się zdarzyć, że wykona się cała kolejka zapytań do bazy, zanim zostanie uruchomiony chociaż jeden listener on('post'...).

Weźmy przykład tego sequence_id, który miał być w testach - wykonujemy bardzo szybko 3 requesty i może się zdarzyć, że wszystkie rekordy będą miały sequence_id = 3. Poza tym mogą się pojawiać chwilowe niespójności bazy danych jak np. dla reverse-single-reference. Na razie mam pomysł, żeby patrzeć na timestampy i ignorować rekordy, które są późniejsze rekordu odpowiadającemu danemu listenerowi.

Pomysł z timestampami jest bardzo dobry - niestety trzeba będzie obciążyć użytkownika Sealiousa pamiętaniem o ich pilnowaniu. W przyszłości będziemy mogli to ładniej rozwiązać korzystając z transakcji w najnowszym mongo :wink:

Też nie jest idealny, bo nie chroni nas przed wbiciem się jakiejś operacji delete pomiędzy modyfikacja jakiegoś rekordu, a listenerem typu post. Choć ta chwilowa niespójność wydaje się mniej upierdliwa.

Obecnie tak jak Kuba zgłosiłeś (good catch!) są niedeterministyczne bugi, ale wydaje mi się że dochodzę do tego co jest problemem. Przy okazji timestampy jednak nie są wcale takie fajne - to jest jeden z problemów - zdarza się, wcale nie tak rzadko, że dla różnych rekordów mamy identyczne timestampy!

Jakby zabrakło mi pomysłów to będę to sygnalizował.

1 Like

Być może powinniśmy robić timestampy oparte nie o milisekundy, ale o mniejsze jednostki? :o

Zmiana jednostek na mniejsze w timestampach poprawi sytuację, ale nie jest pewien czy rozwiąże problem całkowicie.

Ogólnie problem wynika (jak sądzę) z tego jak działa event loop i jak on reaguje na operacje I/O. Obecnie field cached-value działa w ten sposób, że odpytuje bazę, na podstawie wyniku zapytania oblicza wartość cacha i ponownie robi zapytanie updatujące wartość fielda cached-value. Chociażby przy dwóch requestach wysłanych jeden po drugimzamiast oczekiwanego r1,w1,r2,w2 możemy mieć r1,r2,w2,w1 (bo przecież nie ma gwarancji, że mongo odpowie w kolejności na odczyty).

Czy timestampy rozwiążą problem jest dosyć proste do weryfikacji, zajmę się tym. Jeżeli nie to trzeba by pomyśleć nad jakimś event queue -> przetwarzanie kolejnego cached-value nie może się rozpocząć dopóki nie zakończy się przetwarzanie poprzedniego.

O ile dobrze rozumiem, to ten problem pojawia się tylko przy sequence id, prawda? Bo wczytujemy wartość z tej samej kolekcji, którą odczytujemy?

Myślę, że implementowanie własnej kolejki to będzie overkill. Lepiej użyć transakcji w Mongo. Możemy przełączyć się na obraz mongo w wersji beta i pomyśleć, jak to wpasować w strukturę Sealiousa (myślę, że context będzie tutaj przydatny).

Jeżeli cached value będzie działał znośnie dla wartości polegających tylko na zmianach w innej kolekcji, to myślę że można to lądować, i założyć pilnego taska na dołożenie transakcji do Sealiousa.

Niestety nie ma możliwości, aby oprzeć timestampy o mniejsze jednostki. To technicznie niemożliwe. Owszem, mamy process.hrtime(), ale on zwraca czas upływający od innego momentu niż Date.now(), zatem nie można wziąć np. nanosekund pierwszego do drugiego, bo przekręcenie się licznika w jednym czasie nie wpłynie na drugi.

Mam jeszcze jeden pomysł, który zaraz zweryfikuję.

Nie, jednak nie da rady tego zrobić dobrze na ten moment w prosty sposób.

Teraz czy dobrze rozumiem - przeredagować testy tak, aby wstawić drugą kolekcję i w ten sposób zweryfikować czy działa znośnie, obecne wywalić, bo kod i tak zostanie w diffie, tylko zostawić komentarz gdzie tego szukać? I oczywiście założyć taska na transakcje (o ile one rozwiążą ten problem).

1 Like

Dokładnie tak :slight_smile: