Od samego początku istnienia Eltena, program w określonym takcie, który zmieniał się wielokrotnie na przestrzeni lat, odpytywał serwer o powiadomienia i aktualizował sesję jako zwykłe zapytanie HTTP. By szok migracji do Eltena 3.0 nie był aż taki i ograniczyć liczbę błędów, zachowaliśmy to zachowanie także w nowym API, a zmiana tego była planowana na Eltena 3.1.
W związku z tym jednak, że nagłym i zaskakującym mnie zainteresowaniem okazało się tworzenie programów do Eltena, w tym gier real-time, wymaga to szybszej aktualizacji. Dlatego nowe API serwerowe udostępnię w Eltenie 3.0.2.
* Serwer do wymiany danych używa ramek strumienia HTTP/2 Oznacza to, że nie aktualizuje stanu kolejnymi zapytaniami, a informacje o powiadomieniach lub zmianach są dostarczane natychmiast, bez opóźnienia * Jako fallback przy niestabilności protokołu lub wymuszeniu HTTP/1.1 (np. z powodu proxy, ograniczonych sieci Wi-Fi), używana jest strategia long-poll
#StandWithUkraine
Shoot for the Moon. Even if you miss, you'll land among the stars.
Przy braku ACK przez 30 sekund próbuje przeskoku na fallback i po 60 sekundach próbuje ponownego nawiązania łączności przez ramki H2. Jak się nie uda, pozostaje na fallbacku i za kolejne 60 sekund próbuje skoku na nowe API. Itd.
#StandWithUkraine
Shoot for the Moon. Even if you miss, you'll land among the stars.
Jak uruchamiam eltena na nowym commicie, wczoraj niby działało, dziś się sypie potężnie. W sensie codecs twierdzi, że mnożonych jest pełno żądań, dochodzi do limit exceeded i potem już forum, wiadomości, cokolwiek bym nie zrobił wyrzuca błąd. Myślałem, że to mój gameroom robi dziwne akcje i mnoży żądania, ale robiłem testy potem na czysto
Wkleję to, co wypluł codecs, sorki, nieco przydlugie, ale idę do pracy i nie mam jak wyczyścić xd. # Regresja po commicie 9cf8b68 — lawina żądań i „Too many requests”
Data testu: 30 sierpnia 2026 Klient: ELTEN 3.0.1, Windows x64, Ruby 4.0.6 Podejrzany commit: `9cf8b68617c1cc738b7d42d5799cce862491de8e` — „Improve session updating handling, with HTTP/2 streams and long-poll” Commit kontrolny: `a9a96b422a59796ac8c9364aec5e8711aa912f2b` — bezpośredni rodzic `9cf8b68`, również ELTEN 3.0.1
## Streszczenie
Regresję udało się odtworzyć od świeżego uruchomienia ELTEN-a, bez otwierania forum, wiadomości ani Game Roomu.
Po commicie `9cf8b68` klient wysyła około 10 żądań `/api/v1/system/realtime-state` na sekundę przez HTTP/2. Po wyłączeniu HTTP/2 nadal wysyła około 6 żądań na sekundę.
Serwer nie realizuje żądanego `wait_ms=5000`. Poprawna odpowiedź przychodzi po około 72–100 ms, zamiast po około pięciu sekundach. Odpowiedź nie zawiera również pól `realtime_stream` ani `realtime_cursor`.
Po przekroczeniu limitów serwer zwraca HTTP 429 wraz z `Retry-After: 1`, ale klient nadal odpytuje w tym samym tempie. Najpierw uruchamia się limit przeznaczony dla realtime, a następnie ogólny limit uwierzytelnionej sesji. W rezultacie forum, wiadomości i inne funkcje raz działają, a raz zwracają „Too many requests”.
W wersji kontrolnej sprzed commita mechanizm wykonuje około 0,47 żądania na sekundę, zgodnie z lokalnym odstępem dwóch sekund. Forum i wiadomości działają wtedy stabilnie.
## Odtworzenie od zera
1. Zamknięto wszystkie działające kopie ELTEN-a. 2. Uruchomiono jedną kopię ELTEN-a 3.0.1 z commitem `9cf8b68`. 3. Nie otwierano forum, wiadomości ani Game Roomu. 4. Zmierzono wewnętrzny licznik żądań `NotificationService`. 5. Wykonano pojedyncze, kontrolowane próby endpointów realtime, forum i wiadomości. 6. Następnie przeprowadzono identyczny test na bezpośrednio wcześniejszym commicie ELTEN-a 3.0.1.
## Wyniki wersji z commitem 9cf8b68
Klient został uruchomiony o 08:21:52.
O 08:23:05 licznik żądań wzrósł z 660 do 690 w ciągu trzech sekund. Daje to dokładnie 10 żądań na sekundę.
W tym samym momencie `/api/v1/system/realtime-state` zwrócił:
- `stream_supported` było ustawione na `false`; - strumień realtime nie był połączony; - klient nie próbował otwierać strumienia; - liczba awarii strumienia wynosiła zero; - termin następnego żądania, `next_request_at`, pozostawał w przeszłości; - po zakończeniu każdego żądania następne było wysyłane natychmiast.
## Dlaczego właściwy strumień realtime się nie uruchamia?
Long-poll jest tutaj mechanizmem awaryjnym. Docelowo klient powinien przejść na stały strumień HTTP/2.
Negocjacja wygląda następująco:
1. Klient wysyła `stream_capability=1`, informując serwer, że obsługuje strumień. 2. Serwer powinien odpowiedzieć `realtime_stream=1`. 3. Dopiero wtedy klient próbuje otworzyć `/api/v1/system/realtime-stream`.
W rzeczywistej odpowiedzi serwera nie było jednak ani `realtime_stream`, ani `realtime_cursor`.
W konsekwencji klient ustawił `stream_supported = false` i w ogóle nie próbował otwierać właściwego strumienia. Potwierdza to również licznik błędów strumienia równy zero — strumień nie uległ awarii, ponieważ nigdy nie został uruchomiony.
Nie da się z samego klienta rozstrzygnąć, dlaczego backend nie reklamuje strumienia. Możliwe przyczyny to między innymi:
- niewdrożona obsługa po stronie serwera; - wyłączona funkcja; - niezgodna wersja kontraktu klient–serwer; - niepełne wdrożenie zmian backendowych.
Można natomiast jednoznacznie potwierdzić, że klient nie uruchamia strumienia właśnie dlatego, że odpowiedź serwera nie zawiera wymaganej informacji o jego obsłudze.
## Sprawdzenie zachowania long-polla
Aby sprawdzić zachowanie serwera bez aktywnego limitu, automatyczne odpytywanie zostało tymczasowo zatrzymane wyłącznie w pamięci procesu.
Przez 12 sekund licznik żądań nie zmienił się ani razu.
Po wygaśnięciu limitu wysłano jedno żądanie z:
- `wait_ms=5000`; - `stream_capability=1`.
Serwer odpowiedział:
- HTTP 200; - po 0,0718 sekundy; - bez `realtime_stream`; - bez `realtime_cursor`.
Odpowiedź przyszła więc po około 72 ms, a nie po pięciu sekundach.
Dowodzi to, że backend obsługujący tę sesję zachowuje się dla tego endpointu jak wcześniejszy endpoint migawkowy. Nie utrzymuje żądania przez podany czas long-polla.
## Wpływ na forum i wiadomości
Po ponownym uruchomieniu automatycznego odpytywania:
- o 08:25:12 odrzucone zostały jednocześnie odczyt forum i odczyt listy rozmów; - w kolejnych próbach forum było odrzucane stale; - wiadomości działały naprzemiennie — jedna próba przechodziła, kolejna była odrzucana; - licznik automatycznych odpytań wzrósł z 1709 do 3104 między 08:25:12 a 08:27:44.
Bezpośrednie próby wykonane o 08:28:35 zwróciły zarówno dla forum, jak i wiadomości:
To wyjaśnia obserwowane zachowanie, w którym pierwsze żądanie może przejść, kolejne zwraca błąd, a następne znowu działa.
Zwykłe operacje współdzielą limit uwierzytelnionej sesji, który jest stale obciążany przez mechanizm realtime. W zależności od chwilowego stanu okna limitu pojedyncze żądanie może zostać przepuszczone lub odrzucone.
## Próba bez HTTP/2
HTTP/2 został tymczasowo wyłączony wyłącznie w pamięci uruchomionego klienta.
Wyniki:
- worker wykonał 18 żądań w trzy sekundy; - daje to 6 żądań na sekundę; - po dwunastu sekundach ciszy pojedynczy long-poll HTTP/1.1 zakończył się sukcesem po 0,1004 sekundy; - odpowiedź nadal nie zawierała informacji o obsłudze strumienia.
Wyłączenie HTTP/2 zmniejsza tempo, ponieważ HTTP/1.1 ma większy narzut, ale nie usuwa przyczyny błędu.
Problem występuje we wspólnej logice planowania kolejnego odpytywania, a nie w samym transporcie HTTP/2.
## Kontrola A/B na poprzednim ELTEN-ie 3.0.1
Uruchomiono ten sam profil, na tym samym komputerze i serwerze, używając bezpośredniego rodzica commita, czyli `a9a96b4`.
Była to nadal wersja ELTEN 3.0.1, a nie wcześniejszy ELTEN 3.0.
Wyniki:
- licznik żądań wzrósł o 7 w ciągu 15 sekund; - daje to około 0,467 żądania na sekundę; - lokalny `refresh_interval` wynosił dwie sekundy; - cztery kolejne odczyty struktury forum zakończyły się poprawnie; - cztery kolejne odczyty listy wiadomości zakończyły się poprawnie; - nie wystąpił limit.
Test A/B izoluje regresję do zmian wprowadzonych przez `9cf8b68`.
Nie jest to efekt:
- Game Roomu; - serwera MCP; - uruchomienia drugiej kopii programu; - profilu użytkownika; - samego HTTP/2.
## Łańcuch przyczynowy w kodzie
Przed commitem `request_status` zawsze ustawiał lokalny termin następnego żądania:
Commit `9cf8b68` usunął to zabezpieczenie i zaczął polegać na tym, że serwer przetrzyma żądanie przez `wait_ms=5000`.
Obecny przebieg wygląda więc następująco:
1. Klient wysyła żądanie realtime z `wait_ms=5000`. 2. Serwer nie utrzymuje żądania i natychmiast zwraca odpowiedź. 3. Odpowiedź nie reklamuje obsługi strumienia. 4. Klient pozostaje w trybie long-poll. 5. Worker wykonuje swoją pętlę co 0,1 sekundy. 6. Po odebraniu odpowiedzi żądanie zostaje usunięte z listy trwających żądań. 7. `next_request_at` nadal wskazuje czas w przeszłości. 8. Worker natychmiast wysyła następne żądanie. 9. Powstaje około 10 żądań na sekundę.
Dodatkowy problem występuje podczas obsługi odpowiedzi HTTP 429.
Taki obiekt nadal jest Hashem. Funkcja `status_response_data` zwraca cały payload dla każdej odpowiedzi innej niż poprawne:
```json { "success": true, "data": {} } ```
Następnie `handle_status_response` uznaje każdy Hash za prawidłowe dane stanu.
W efekcie:
- `success: false` nie jest rozpoznawane jako błąd; - nie uruchamia się `schedule_poll_error_retry`; - klient nie wykonuje backoffu; - nadal wysyła kolejne żądania.
Jest jeszcze problem z nagłówkami HTTP.
Żądanie statusu przekazuje do warstwy HTTP tablicę:
```ruby [key, request_id] ```
Warstwa HTTP zapisuje nagłówki odpowiedzi tylko wtedy, gdy przekazane `data` jest Hashem.
W rezultacie nagłówek `Retry-After: 1` nie trafia do `NotificationService`, więc ten mechanizm nie może go wykorzystać nawet po dodaniu odpowiedniej obsługi kodu 429.
## Najważniejsze miejsca w kodzie
- `src/eapi/notifications.rb:207` — pętla workera wykonywana co 0,1 sekundy; - `src/eapi/notifications.rb:237` — wysłanie następnego żądania, gdy termin minął i nie ma żądania w toku; - `src/eapi/notifications.rb:365` — `request_status`, bez ustawienia przyszłego `next_request_at`; - `src/eapi/notifications.rb:386` — wysyłanie `wait_ms=5000`; - `src/eapi/notifications.rb:664` — obsługa odpowiedzi; - `src/eapi/notifications.rb:695` — retry uruchamiany jedynie dla rozpoznanych błędów transportu lub parsowania; - `src/eapi/notifications.rb:1222` — odpowiedź błędu pozostaje zwykłym Hashem; - `src/eapi/http.rb:119` — nagłówki odpowiedzi są dołączane tylko wtedy, gdy `data` jest Hashem.
## Wniosek
Commit zakłada kompatybilny backend long-poll/stream, którego bieżąca odpowiedź produkcyjna nie potwierdza.
Problem ma dwie warstwy:
1. Właściwy strumień realtime nie jest uruchamiany, ponieważ serwer nie reklamuje jego obsługi. 2. Awaryjny long-poll nie ma bezpiecznego ograniczenia tempa, gdy serwer ignoruje `wait_ms`.
Niezależnie od planowanego wdrożenia lub poprawienia serwera klient nie ma obecnie bezpiecznej obsługi starszej albo niekompatybilnej odpowiedzi.
Nie rozpoznaje też odpowiedzi `success: false` jako błędu wymagającego odczekania.
## Co warto zweryfikować w poprawce
- przywrócenie lokalnego minimalnego odstępu pomiędzy żądaniami fallbacku; - jawne rozpoznawanie `success: false`; - obsługę HTTP 429; - respektowanie `Retry-After`; - bezpieczny backoff przy kolejnych błędach; - uruchamianie strumienia wyłącznie po jednoznacznej reklamie jego obsługi; - test zgodności z backendem, który ignoruje `wait_ms`; - test zgodności z backendem, który nie zwraca `realtime_stream` ani `realtime_cursor`; - sprawdzenie po stronie serwera, dlaczego bieżąca odpowiedź nie reklamuje strumienia realtime.
W trakcie diagnostyki nie zmieniano plików źródłowych i nie wdrażano żadnej poprawki.