Czemu mi srand nie działa

Czasami człowiek spędzi dwa tygodnie na durny błąd, którego testy ani wszelkie debugowanie w żaden sposób nie zweryfikują. O co poszło tym razem? Wcisnąłem parę dinemic i jego wrapper dla pythona do Flaska. Ot co, alternatywa dla ORM Django i SqlAlchemy, w dodatku nie trzeba się martwić o uprawnienia, wewnętrzne API i tak dalej…

Wszystko szło dobrze, natomiast po dłuższym użytkowaniu zauważyłem, że czasami obiekty w bazie nie chcą się tworzyć, może w 1 na 10 lub na 20 przypadkach. Winny oczywiście Sync processor, który prawdopodobnie miał lukę gdzieś w zawiłej logice odpowiedzialnej za procesowanie zmian w klastrze. Zwykle takich problemów nie ma, gdy za każdym mikroserwisem stoi baza danych i tylko ten mikroserwis ją modyfikuje (ewentualnie kilka jego wątków, korzystając z locków). Dinemic daje nieco inne podejście, gdzie każdy może rozesłać zmianę bazy, a dopiero logika naszego programu ma stwierdzić, czy taką zmianę akceptuje, czy nie. W praktyce, ciąg zmian związany ze stworzeniem i cyklem życia obiektu zamiast tak:

wygląda tak:

Każdy bloczek w powyższym to mała wiadomość wędrująca po całym klastrze z informacją np. w polu A zmień wartość na X. Kto ją dostanie, może zaaplikować u siebie, zweryfikować podpis obiektu, który zmianę wykonał lub sprawdzić ciągłość całego łańcucha (lub drzewka). Taka komplikacja frameworka miała na celu umożliwienie pracy w niekonsystentnym środowisku, na rozproszonej bazie. Czemu zmiany są aż tak pokomplikowane? Pomyśl, że chcesz się umówić na piwo z 10 znajomymi. Wysyłasz wszystkim SMS, że w piątek o 20 idziecie na piwo. 6 odpisze, że ok, 1 w między czasie umówi się na inne piwo i za godzine odpisze, że jest zajęty, a pozostałych trzech jest poza zasięgiem. To jesteście umówieni? Na piwo trzeba iść, a z pozostałą czwórką umówić się w innym terminie lub skontaktować inaczej. Zwykle rozwiązanie zależy od “mózgu” operacji i tego co zrobi z pozostałą czwórką. Ot, takie życie. Podobne założenia były w dinemic – kto dostanie informacje o zmianie w bazie, ten (jakoś) zareaguje, a kto nie dostanie, ten musi radzić sobie sam. W takiej architekturze już na wstępie mamy mocno niekonsystentną bazę w obrębie klastra, a jedyne co powinno się zgadzać to cała historia zmian.

Wróćmy do naszych wiadomości o zmianach (sms z zaproszeniem na piwo). Jako, że wspólnego, konsystentnego indeksu nie mamy, to każda zmiana ma losowy ID, do tego hash poprzedniej zmiany oraz swój podpis. Problem z Flaskiem rozpoczął się dość niewinnie – jakby niektóre zmiany nie były poprawnie procesowane i ignorowane. Po prostu system twierdził, że wiadomość była już przetworzona.

Wróćmy na chwilę do Flaska i tego, jak działa serwer www. Flask przy uruchamianiu serwera przez skrypt pythona tworzony jest główny wątek. W nim można m.in. zainicjalizować framework dinemic (lub cokolwiek innego) i przygotować wszystko co potrzeba do odpalenia serwisu. Po otrzymaniu nowego połaczenia od klienta serwer tworzy proces potomny i w nim obsługuje request.

Niby proste, a jednak nie. Jeśli wszystko inicjalizujemy w głównym wątku, to również generator liczb losowych (pseudolosowych), czyli srand(time(NULL)). Niezależnie od tego jaką wartość podamy do srand, to powinien dać pseudolosowe liczby, o dobrym rozkładzie. Jeśli jednak zainicjalizujemy go dwukrotnie tą samą wartością, to zawsze dostaniemy taką samą serię liczb.

W czym zatem problem? Srand inicjalizujemy w głównym wątku i generuje on X liczb pseudolosowych. Dla każdego klienta Flask tworzy nowy wątek, w którym również potrzebne są nowe liczby losowe (bo na przykład każdy update generowany w takim wątku potrzebuje swoje ID). Czym to skutkuje? Po forku proces potomny generuje X+1 liczbę losową. Dla drugiego klienta fork również generuje liczbę, uwaga uwaga: X+1. I tak dalej… rand wypluwa zawsze pseudolosową, kolejną liczbę z generowanego ciągu. Jeśli skopiujemy proces (przez fork, pthread z odseparowaną pamięcią lub jakikolwiek sposób), to licznik zaczyna się dublować z innymi procesami. W ten sposób każdy proces potomny dostaje ten sam ciąg kolejnych liczb pseudolosowych, dla danej wartości początkowej z srand.

Dzięki temu możnaby ze 100% skutecznością zgadywać z procesu-brata jakie liczby wylosuje drugi proces. Na zakończenie warto jeszcze opowiedzieć jak udało się to rozwiązać (chociaż po znalezieniu problemu nie było to trudne). Rand z C++ został zamieniony na generator liczb losowych z libsodium. Praktycznie bez zmiany logiki, algorytmów i dużego grzebania w kodzie. Dużym plusem jest to, że libsodium nie wymaga inicjalizowania generatora liczb pseudolosowych i zwraca za każdym razem losowe wartości, co w przypadku biblioteki takiej jak dinemic ma duże znaczenie – nigdy nie wiadomo czy aplikacja macierzysta nie zacznie się forkować.

Ot, głupi błąd, 2 tygodnie szukania i przy okazji zrefactorowany najpaskudniejszy kawałek logiki frameworka.