Salutare tuturor. Am o problemă pe un proiect care mă scoate din minți de două zile și am nevoie de o a doua pereche de ochi, fiindcă simt că mă învârt în cerc.
Aplicația e un dashboard B2B pe care avem în jur de 3.200 de useri activi pe zi. De vreo săptămână, suportul a început să fie bombardat cu tichete de la clienți nervoși că sunt scoși afară din cont din senin în timp ce completează formulare lungi. Chestia ciudată e că se întâmplă destul de rapid, la 5 sau 10 minute de la autentificare.
La prima vedere, părea clasicul caz de cookie expirat prost. M-am uitat în DevTools, dar cookie-ul de sesiune pleacă cu Max-Age=2592000 (30 de zile), are SameSite=Lax, Secure, HttpOnly și un Path=/. Pe hârtie, totul e conform manualului.
Ce am eliminat deja din listă
Stack-ul are un backend în Node/Express în spatele unui Nginx pe post de reverse proxy, cu sesiunile salvate într-o instanță separată de Redis. Sesiunile nu stau în memoria procesului, așa că am zis inițial că poate crapă vreun container și se pierde starea. Nu e cazul.
Am verificat următoarele puncte și par complet curate:
- Redis memory și evicțiuni: Redis-ul rulează relaxat, stă pe la 18% memorie folosită dintr-un cluster mic de 2GB. Niciun
evicted_keysînINFO stats. TTL-ul pe cheia de sesiune din Redis este setat identic cu cookie-ul, adică 30 de zile, cu prelungire automată la fiecare request (rolling: true). - Cluster / Load Balancer: Avem două instanțe de Node.js în spatele lui Nginx. Chiar dacă traficul sare de pe o instanță pe alta (nu avem sticky sessions activate), ambele lovesc aceeași bază de date Redis. Când citesc sesiunea manual prin CLI din Redis cu ID-ul din cookie, cheia e acolo.
- Debian/PHP Garbage Collector: Știu că pe PHP clasic există vechiul bug cu cron-ul de sistem care dă wipe la
/var/lib/php/sessionsla 24 de minute indiferent ce pui însession.gc_maxlifetime. Dar aici suntem 100% pe Node cuconnect-redis, deci n-avem treabă cu fișiere temporare de OS.
Unde suspectez că e buba
Am reușit să reproduc comportamentul o singură dată pe contul meu de test. Eram pe o pagină cu un grafic greu care face 4 request-uri paralele prin fetch către API-uri interne. Când am dat refresh, sesiunea s-a evaporat instant și am primit 401 pe linie.
Mă gândesc serios la un race condition pe session save. Când browserul trage 4 request-uri simultan, toate trimit același session ID, dar dacă unul din ele modifică starea sesiunii (sau dacă middleware-ul încearcă să regenereze ID-ul pentru securitate la fiecare hit), cel mai lent request ar putea suprascrie sau invalida ID-ul anterior.
O altă bănuială e legată de Nginx și buffering. Dacă clientul trimite un chunk mare de date și conexiunea se întrerupe, oare e posibil ca header-ul de Set-Cookie să fie trimis gol sau corupt pe vreun retry intern?
Trade-off-ul la mecanismul de rolling sessions e că de fiecare dată când userul atinge o pagină, faci scriere în Redis. Pe volum mic e super ok, pe volume mari poți ajunge la write locks inutile sau suprascrieri accidentale.
A mai lovit cineva o chestie similară? Dacă v-ați bătut de sesiuni invalidate fantomă la câteva minute deși cookie-ul trăiește liniștit în browser, unde ați găsit buba până la urmă?