PHP sessions represent a very common practice to acquire data and user persistency across HTTP requests. While in 99% of time they are successfully used without any problems, there are some cases, when a few planets align, when they can quickly get you into problems.
AJAX requests are usually used to load or to send pieces of data asynchronously so to keep the application responsive. Most of the times they consume fast running scripts on the back end that quickly return to browser. This is very good and that’s the way they should be done. By back end script I mean a PHP script in this case, but there can be other technologies as well: Java, .Net, etc. By fast script I mean (let’s say) a 100ms runtime on server.
However there can be some cases when the back-end script runs very slow as it is doing heavily computations or is waiting for some 3rd party service/gateway to return. In such cases the runtime on server can quickly become 10s or even more. The “magic” happens when these long-waiting AJAX requests are making use of PHP sessions. In this particular case, you will notice how subsequent AJAX requests or even non-AJAX requests will hang and wait for the first call to complete. The first instinct will be to double check the client code that fires up the AJAX calls, but that is certainly not the cause for this odd behavior.
The main responsible here is PHP and the underlying session handling mechanism. This applies to default session storage (filesystem) and to default session handler function. What happens is that when PHP opens a session file (so once session_start is called) it acquires a lock on that file and thus making the other (AJAX) requests to wait for the file lock to be released. The default session handler uses flock with LOCK_EX flag (exclusive file lock).
A way around to this problem would be to create a custom session handler and, in read method, make sure to open the session file without locking it. A simple call to fopen or file_get_contents will be enough (these apply for filesystem storage only). This method has a downside though: given that no file lock is acquired, the session data might become inconsistent in case multiple processes write to it. In case you only need to read data from multiple processes, then you’re covered with this solution.
To conclude: all of the above applies for the case when your AJAX calls consume long running scripts on server, backed by PHP sessions with default session storage and handler.
A very detailed article about race conditions with AJAX and PHP Sessions can be found here.
Session garbage collection is the operation through which PHP deletes expired session files on the server. Usually this happens through a PHP process determined by session.gc_probability setting in php.ini. The value of this setting along with session.gc_divisor indicate the chance that a PHP process has to “play the role” of garbage collector.
However on a few Linux distributions (Debian and Ubuntu), the PHP garbage collection process is disabled (session.gc_probability = 0) and replaced with a system cron. The later does exactly the same job as the PHP process.
The “twist” appears when one edits the value of session.gc_probability by changing it to something different than 0 while maintaing the aforementioned cron. This can easily happen if the one doing the change is unaware of how things work on Ubuntu/Debian distributions. From now on PHP understands that it needs to do garbage collection as well. Again, we are talking about filesystem based session storage as this would not apply for database, memory and memcached storage options.
What will happen, very sporadically, would be that the users triggering the PHP garbage collection process will see some PHP warnings on the screen (in case display_errors is on) indicating a session related error. The error itself is mainly related to two things: one being a rights related problem (for reading the session dir) and the other one, which assumes the rights problem is fixed, is related to concurrent access to a given session file.
The former, which is more frequent, yields a message something along these lines: “Error #8 session_start(): ps_files_cleanup_dir: opendir(/var/lib/php5) failed: Permission denied”. This is because, when used with default configurations, PHP does not have read access to session directory as it was supposed to be cleaned by the cron only. So, the read/write rights are granted to the user the cron is running on only. Usually the root. This problem was initially reported by a Ubuntu developer on his blog.
The other error, the one with concurrent access, also presents itself with a “Permission denied” message and is caused by the both garbage collection methods accessing the file at the same time. This is the system cron and that PHP process that acts as a garbage collector.
The solution to this problem is very simple, which is to keep a single garbage collection method only.
Like already mentioned this only happens on Debian/Ubuntu distributions, when activating the PHP process garbage collection together with the system cron.
Wrap it up
- long waiting AJAX calls baked by sessions can make your application less responsive
- on Ubuntu/Debian, make sure to avoid garbage collection session file conflicts