I had a very embarrassing experience last month when users of my web app, which I had recently upgraded from Laravel 4.0 to Laravel 4.1, were being randomly logged out.
Searching through the Laravel Forums I quickly came to the conclusion that the revised session configuration in Laravel 4.1 was relevant to the problem and, after trying several values for the ‘lifetime’ and ‘expire_on_close’ fields, I eventually came up with a combination that worked.
However, it irked me that this problem had occurred in the first place and I wondered how I could have made such a stupid mistake, so today I spent some time researching the problem in an attempt to solve the mystery.
And solve it I did. What follows is a detailed account of what went wrong for me along with an explanation of why it happened and how I resolved the issue. I have published my notes here in the hope that it allows others to avoid making the same mistake with their 4.0 to 4.1 migration.
In Laravel 4 (and possibly Laravel 3, but that is too long ago to remember now :-), there was only one configuration option for session time-outs in app/config/session.php
:
/*
|--------------------------------------------------------------------------
| Session Lifetime
|--------------------------------------------------------------------------
|
| Here you may specify the number of minutes that you wish the session
| to be allowed to remain idle for it is expired. If you want them
| to immediately expire when the browser closes, set it to zero.
|
*/
’lifetime’ => 120,
As I was using the native session manager, all Laravel essentially did was set PHP’s session.cookie_lifetime
setting to whatever was specified in ‘lifetime’ and change the ‘session.save_path’ to app/storage/sessions
. Garbage collection was left to PHP, so old session files hung around for the number of minutes specified by ‘session.gc_maxlifetime’. (As a side note, whilst Laravel 4.0 did set the values of ‘session.gc_probability’ and ‘session.gc_divisor’, because I was running on Debian these values were ignored, as Debian uses a cron job specified in /etc/cron.d/php5
that runs every 30 minutes and simply deletes all sessions older than the value specified in ‘session.gc_maxlifetime’).
Laravel 4.1 added a new option and a slightly modified comment:
/*
|--------------------------------------------------------------------------
| Session Lifetime
|--------------------------------------------------------------------------
|
| Here you may specify the number of minutes that you wish the session
| to be allowed to remain idle before it expires. If you want them
| to immediately expire on the browser closing, set that option.
|
*/
’lifetime’ => 120,
'expire_on_close' => false,
Although the comments had been updated to reflect the new option, there was no indication that Laravel’s utilization of the ‘lifetime’ field had been extended in a fairly major way. But more on that later...
Due to the fact that I wanted my clients’ sessions to expire as soon as they closed their browsers, my Laravel 4.0 session configuration looked like this:
'lifetime' => 0,
When I upgraded to Laravel 4.1, I read the comment and, based on my requirements, made the logical decision to leave ‘lifetime’ set to zero and set ‘expire_on_close’ to true. As a result, my Laravel 4.1 session configuration now looked like this:
'lifetime' => 0,
'expire_on_close' => true,
The problem with these settings isn’t obvious until you dig into the Laravel code and figure out what has changed between 4.0 and 4.1. Among other things, session handling received a major overhaul, ditching native PHP sessions entirely. One of the most significant impacts of this change is that Laravel 4.1 now looks after session garbage collection. And guess how it figures out how many minutes to keep old sessions for? You got it, by utilizing the ‘lifetime’ setting above which, BTW, still retains it’s previous responsibility of determining the expiry time in the cookie as well (when ‘expire_on_close’ is set to false that is).
So, to recap, in Laravel 4.0 session garbage collection (for native sessions anyway) was handled natively by PHP and although Laravel overwrote a whole bunch of session-related PHP settings, the one setting it did not overwrite was ‘session.gc_maxlifetime’. Setting Larvel’s ‘lifetime’ option to zero was equivalent to setting ‘session.gc_cookie_lifetime’ to zero in the PHP ini file in order to make the cookie session-only and server-side session lifetime was basically determined by the value of PHP’s ‘session.gc_maxlifetime’ setting.
However, Laravel 4.1 implements its own garbage collection and uses the value of ‘lifetime’ (along with the ‘lottery’ settings, which are the equivalent to PHP’s probability and divisor settings) to control garbage collection. Laravel runs a lottery every time it finishes processing a request and if the right number comes up, it deletes all sessions that are older than the number of minutes specified by ‘lifetime’.
The default values for the lottery are 2 and 100. So basically, regardless of the value of ‘expire_on_close’ and the validity or timeliness of the session cookie provided by the client, if ‘lifetime’ is set to a value of zero, all sessions will get deleted at the server side approximately one in every 50 requests. This explains why my clients were getting logged out literally at random!
I have since changed the value of ‘lifetime’ in my Laravel session configuration from zero to 1440 and left ‘expire_on_close’ set to true. The new session implementation is really, really nice and will avoid a whole heap of platform-specific session configuration woes, which is great, but honestly, a slightly more informative comment about the expanded role of the ‘lifetime’ configuration option would have been nice. :-)