27 Nov 2022 - by 'Maurits van der Schee'
Session locking is well explained by Mattias Geniar in his article "PHP Session Locking: How To Prevent Sessions Blocking in PHP requests" (please read that first). Now that you've done that you know what session locking is. My story is that I have once been bitten by this bug on a high traffic website while following best practices of a popular framework (CakePHP). I went on to do research and found that most major PHP frameworks (such as Symfony) do NOT lock sessions (while PHP natively does do this).
Session locking prevents your application from overwriting and losing random session modifications. Mattias Geniar writes in his article:
"What value is in the session? The value from script 1. The data stored by script 2 is overwritten by the last save performed in script 1. This is a very awkward and hard to troubleshoot concurrency bug. Session locking prevents that."
Anthony Ferrara (username "ircmaxell") agrees on Stackoverflow saying:
"You don't want to disable it... If you do, you'll potentially run into all sorts of weird issues"
Note that top-level navigation is serialized by the browser, so the bug mainly appears when PHP code that writes to the session is executed in AJAX calls. The performance downsides of locking that people mention can be worked around by implementing the "SessionUpdateTimestampHandlerInterface" and closing the session with
session_write_close immediately after starting it with
session_start. The major frameworks might consider doing this automatically on GET (AJAX) requests (and have some override option). For some background on this topic read my blog post titled "Proposal to fix a 2012 bug in Symfony".
If you want to use my implementation of the Redis session save handler you need to put this before your call to "
ini_set('session.save_path', 'tcp://localhost:6379'); ini_set('session.use_strict_mode', true); session_set_save_handler(new RedisSessionHandler(), true);
The source code for the RedisSessionHandler class can be found on Github, see: https://github.com/mintyphp/session-handlers/blob/main/src/RedisSessionHandler.php
Testing concurrency scenarios is not the easiest thing to program and that's why I had so much fun doing just that. I wrote a test suite to ensure that I could reproduce the data loss scenario that Mattias Geniar describes in his post. Here is what the test suite does:
It executes a HTTP request that starts the session and sets a session variable to 1. After that it does 9 concurrent (parallel) requests (using multi-curl) to increment that session variable. If locking works correctly the session variable is set to 10 after the 10 requests. Without locking all 9 concurrent requests read the value as 1 and write it as 2 (incremented). This results in a session variable being set to 2 after all 10 requests.
Then I wrote an call intercepting logger allow me to record the session handler calls of the known good (native) implementation that can be executed using:
session_set_save_handler(new LoggingSessionHandler(new \SessionHandler()), true);
After that I found a reference on the implementation of a session save handler in the test suite of the PHP repository on Github. All pieces came together nicely and allowed me to create some more locking PHP session save handlers that can actually be tested for correctness.
Source code: https://github.com/mintyphp/session-handlers
The same testing mechanism can be applied to the existing Session handlers in Symfony. So I created a similar test suite to show the (lack of) locking of the Symfony session save handlers. Note that some of the existing handlers fail the test (as expected).
Source code: https://github.com/mevdschee/symfony-session-tests