BLACKBELT

MongoSession – A PHP MongoDB Session Handler

About

MongoSession is a PHP implementation of a MongoDB session wrapper. This class was built as a drop-in for easily switching to handling sessions using Mongo. It’s a great replacement to memcache(d) for VPS servers where you risk memory being reshuffled in the pool and taking performance hits.

A Word of Warning

Based on a new blog post entitled MongoDB Performance & Durability, you should be made fully aware that the current stable release of MongoDB, 1.4.4, risks corrupting or losing session data. There are no guarantees of 100% reliability. If you are familiar with this topic, please chime in in the comments section for clarification. For the rest of you, please consider reading the linked article. Certain preventative measures are available to reduce this risk. Possibilities include setting up a replica pairs (replica sets will be available in 1.6). Alot of the concerns are minor when we’re talking about simple user login validation. You might be risking a maximum of losing a minute’s worth of data in the worst case and forcing a re-login.

I have recently added two boolean constants to the library which allow you to specify whether you’d like to increase the apparent consistency of the session handler. You may toggle both FSYNC and SAFE to ensure session data is written to disk before returning.

In the comments section, PHPGangsta has also reported that the default session handler uses file locking to avoid race conditions. He links to a great article, Race Conditions with AJAX and PHP Sessions, which spells out the issue and the possible negative outcomes of not implementing locking.

I have recently updated the library to support atomic operations on both session writes and garbage collection to help prevent these race conditions.

Download

MongoSession is currently hosted on github. Click here to be taken to the github page. For those of you wishing to do a quick git clone, please use the following link:

[sourcecode light=”true”]
http://github.com/cballou/MongoSession.git
[/sourcecode]

The rest of you can automatically download the library by clicking this link.

Source Code

Some of you may just be interested in a quick glimpse of the implementation. I’ve got you covered.

[sourcecode lang=”php”]
<?php
/*
* This MongoDB session handler is intended to store any data you see fit.
*/
class MongoSession {

/**
* Whether session writes should be performed safely. If TRUE, the
* program will wait for a database response and throw a
* MongoCursorException if the update failed. Can also be set to an
* integer value for replication. For more information, see:
* http://www.php.net/manual/en/mongocollection.update.php
* Slower when on but minimizes any session errors when coupled with FSYNC.
*/
const SAFE = false;

/**
* If TRUE, forces the session write to be synced to disk before
* returning success.
*/
const FSYNC = false;

// example config with support for multiple servers
// (helpful for sharding and replication setups)
protected $_config = array(
// cookie related vars
‘cookie_path’ => ‘/’,
‘cookie_domain’ => ‘.mofollow.com’, // .mydomain.com

// session related vars
‘lifetime’ => 3600, // session lifetime in seconds
‘database’ => ‘session’, // name of MongoDB database
‘collection’ => ‘session’, // name of MongoDB collection

// array of mongo db servers
‘servers’ => array(
array(
‘host’ => Mongo::DEFAULT_HOST,
‘port’ => Mongo::DEFAULT_PORT,
‘username’ => null,
‘password’ => null,
‘persistent’ => false
)
)
);

// stores the mongo db
protected $mongo;

// stores session data results
private $session;

/**
* Default constructor.
*
* @access public
* @param array $config
*/
public function __construct($config = array())
{
// initialize the database
$this->_init(empty($config) ? $this->_config : $config);

// set object as the save handler
session_set_save_handler(
array(&$this, ‘open’),
array(&$this, ‘close’),
array(&$this, ‘read’),
array(&$this, ‘write’),
array(&$this, ‘destroy’),
array(&$this, ‘gc’)
);

// set some important session vars
ini_set(‘session.auto_start’, 0);
ini_set(‘session.gc_probability’, 1);
ini_set(‘session.gc_divisor’, 100);
ini_set(‘session.gc_maxlifetime’, $this->_config[‘lifetime’]);
ini_set(‘session.referer_check’, ”);
ini_set(‘session.entropy_file’, ‘/dev/urandom’);
ini_set(‘session.entropy_length’, 16);
ini_set(‘session.use_cookies’, 1);
ini_set(‘session.use_only_cookies’, 1);
ini_set(‘session.use_trans_sid’, 0);
ini_set(‘session.hash_function’, 1);
ini_set(‘session.hash_bits_per_character’, 5);

// disable client/proxy caching
session_cache_limiter(‘nocache’);

// set the cookie parameters
session_set_cookie_params($this->_config[‘lifetime’],
$this->_config[‘cookie_path’],
$this->_config[‘cookie_domain’]);
// name the session
session_name(‘mongo_sess’);

// start it up
session_start();
}

/**
* Initialize MongoDB. There is currently no support for persistent
* connections. It would be very easy to implement, I just didn’t need it.
*
* @access private
* @param array $config
*/
private function _init($config)
{
// ensure they supplied a database
if (empty($config[‘database’])) {
throw new Exception(‘You must specify a MongoDB database to use for session storage.’);
}

if (empty($config[‘collection’])) {
throw new Exception(‘You must specify a MongoDB collection to use for session storage.’);
}

// update config
$this->_config = $config;

// generate server connection strings
$connections = array();
if (!empty($this->_config[‘servers’])) {
foreach ($this->_config[‘servers’] as $server) {
$str = ”;
if (!empty($server[‘username’]) && !empty($server[‘password’])) {
$str .= $server[‘username’] . ‘:’ . $server[‘password’] . ‘@’;
}
$str .= $server[‘host’] . ‘:’ . $server[‘port’];
array_push($connections, $str);
}
} else {
// use default connection settings
array_push($connections, Mongo::DEFAULT_HOST . ‘:’ . Mongo::DEFAULT_PORT);
}

// load mongo servers
$mongo = new Mongo(‘mongodb://’ . implode(‘,’, $connections));

// load db
try {
$mongo = $mongo->selectDB($this->_config[‘database’]);
} catch (InvalidArgumentException $e) {
throw new Exception(‘The MongoDB database specified in the config does not exist.’);
}

// load collection
try {
$this->mongo = $mongo->selectCollection($this->_config[‘collection’]);
} catch(Exception $e) {
throw new Exception(‘The MongoDB collection specified in the config does not exist.’);
}

// ensure we have proper indexing on the expiration
$this->mongo->ensureIndex(‘expiry’, array(‘expiry’ => 1));
}

/**
* Open does absolutely nothing as we already have an open connection.
*
* @access public
* @return bool
*/
public function open($save_path, $session_name)
{
return true;
}

/**
* Close does absolutely nothing as we can assume __destruct handles
* things just fine.
*
* @access public
* @return bool
*/
public function close()
{
return true;
}

/**
* Read the session data.
*
* @access public
* @param string $id
* @return string
*/
public function read($id)
{
// retrieve valid session data
$expiry = time() + (int) $this->_config[‘lifetime’];

// exclude results that are inactive or expired
$result = $this->mongo->findOne(
array(
‘_id’ => $id,
‘expiry’ => array(‘$gte’ => $expiry),
‘active’ => 1
)
);

if ($result) {
$this->session = $result;
return $result[‘data’];
}

return ”;
}

/**
* Atomically write data to the session.
*
* @access public
* @param string $id
* @param mixed $data
* @return bool
*/
public function write($id, $data)
{
// create expires
$expiry = time() + $this->_config[‘lifetime’];

// create new session data
$new_obj = array(
‘_id’ => $id,
‘data’ => $data,
‘active’ => 1,
‘expiry’ => $expiry
);

// check for existing session for merge
if (!empty($this->session)) {
$obj = (array) $this->session;
$new_obj = array_merge($obj, $new_obj);
}

// atomic update
$query = array(‘_id’ => $id);

// update options
$options = array(
‘upsert’ => true,
‘safe’ => MongoSession::SAFE,
‘fsync’ => MongoSession::FSYNC
);

// perform the update or insert
try {
$this->mongo->update($query, array(‘$set’ => $new_obj), $options);
} catch (Exception $e) {
return false;
}

return true;
}

/**
* Destroys the session by removing the document with
* matching session_id.
*
* @access public
* @param string $id
* @return bool
*/
public function destroy($id)
{
$this->mongo->remove(array(‘_id’ => $id), true);
return true;
}

/**
* Garbage collection. Remove all expired entries atomically.
*
* @access public
* @return bool
*/
public function gc()
{
// define the query
$query = array(‘expiry’ => array(‘:lt’ => time()));

// specify the update vars
$update = array(‘$set’ => array(‘active’ => 0));

// update options
$options = array(
‘multiple’ => TRUE,
‘safe’ => MongoSession::SAFE,
‘fsync’ => MongoSession::FSYNC
);

// update expired elements and set to inactive
$this->mongo->update($query, $update, $options);

return true;
}

}
[/sourcecode]

Usage

Basic usage for a single MongoDB database on localhost is as simple as the following:

[sourcecode lang=”php”]
<?php
require_once(‘MongoSession.php’);
$session = new MongoSession();
?>
[/sourcecode]

More advanced setups including sharding, replication, or multiple MongoDB servers will require you to pass a configuration array to the class. An example of the configuration array is as follows:

[sourcecode lang=”php”]
require_once(‘MongoSession.php’);

// the config array for loading MongoDB servers
$_config = array(
// cookie related vars
‘cookie_path’ => ‘/’,
‘cookie_domain’ => ‘.mydomain.com’, // .mydomain.com

// session related vars
‘lifetime’ => 3600, // session lifetime in seconds
‘database’ => ‘session’, // name of MongoDB database
‘collection’ => ‘session’, // name of MongoDB collection

// array of mongo db servers
‘servers’ => array(
array(
‘host’ => Mongo::DEFAULT_HOST,
‘port’ => Mongo::DEFAULT_PORT,
‘username’ => null,
‘password’ => null,
‘persistent’ => false
)
)
);

$session = new MongoSession($_config);
[/sourcecode]

  • http://www.phpgangsta.de PHPGangsta

    Am I right that this session handler also has problems with race conditions like nearly every other session handler? I don’t see any locking mechanism, so I expect problems here.

    I don’t know how to implement that feature with mongodb, but many websites use AJAX and will have problems.

    A good article describing that problem:
    http://thwartedefforts.org/2006/11/11/race-conditions-with-ajax-and-php-sessions/

    So be prepared to have problems when switching from php file session handler to any other handler which doesn’t use locking mechanisms.

    • http://www.jqueryin.com cballou

      Thanks for the post, a variation of row-level locking is definitely something I’ll be looking to implement in the class. In the meantime, I’ll throw a warning on the page regarding the race conditions.

      To sum it up for those of you reading, the built-in session handler uses file level locking with flock() in order to avoid any write contention. The article is definitely worth a read for those of you who use session_set_save_handler or use a particular framework. There is a caveat that the built in handler does not inherently support multiple web servers for larger applications. This is primarily the reason many people are comfortable with using the built-in session handling support of memcache or memcached.

      In the case of MongoDB, there is no direct support of traditional locking mechanisms seen in most RDBMS. There are a number of atomic operations permitted, however, which would lead me to believe there is still a viable solution. I’ll get on the #mongodb freenode channel at some point to find the best suggested operation for session handling and update the class accordingly.

  • Pingback: Paul M. Jones » Blog Archive » Universal Constructor Sighting “In The Wild”()

  • http://tyrael.hu Tyrael

    If you have atomic operations, you can create some semaphore based solution, but you have to do the polling for checking/claiming the semaphore.

    Tyrael

  • http://www.jamendo.com/ sylvain

    Hi !

    Question about your implementation : why not use the built-in _id field for the session id ? according to the mongodb doc, it will be created anyway meaning that you will have both id and _id indexing your collection.

    cheers!

    • http://www.jqueryin.com cballou

      Good observation, sylvain. The change is minimal and would have the net result of using alot less storage and reducing insert/update times due to utilizing one less index. I’ll have the class updated shortly and push the change to github.

  • http://rozacki@gmail.com Chris

    I think the only solution to lack of locking on both PHP and MongoDB is to implement additional level of access that servers session data from a database that is dedicated only for this type of data. This is ugly but don’t think that mongo guys will agree on changing mongo requirements, design and implementation to provide such a locking.

    • http://www.jqueryin.com cballou

      There is still a glimmer of hope for a binary semaphore based solution using MongoDB’s findAndModify() command similar to their queue example. Downsides include having to perform an extra write to update the binary semaphore and also having to implement busy waiting in PHP for “locks” that haven’t been released.

  • http://geekmonkey.org halfdan

    Hey, I’ve forked your project on Github and implemented persistent connections. Have a look at it under: http://github.com/halfdan/MongoSession

  • http://www.devcomments.com/ Jonny

    Please consider security implications of storing session data in a database that does not use username/password for connection. On a shared server anyone can connect to your database and read all your session data. Often times the sensitive user info like profile, email, password will be stored in session. Just understand that if a hacker can connect to your Mongo database, he can read and change! any of the user session

    • halfdan

      If a hacker is able to connect to your database you have a bigger problem with your server than him changing a few sessions..

      Setting up MongoDB with authentication is of course recommended if you store _any_ kind of critical data inside of your database.

      • http://www.jqueryin.com Corey Ballou

        Very true. If you are not using MongoDB authentication it is safest to use alternative measures to block your chosen MongoDB port and only allow access on localhost (127.0.0.1). Generally users on a shared host don’t have access to even install MongoDB, so this really shouldn’t be a problem. Users on a VPS have their own sandboxes but should still definitely restrict access. For those interested, you will most likely want to find instructions to block access to port 27017 using IPTables or an equivalent.

  • T

    try{ $m = new Mongo(); } catch( Exception $e ) { throw new Exception(‘Can’t connect MongoDB server’); }

  • T

    also implement flexihash for redundancy over multiple servers.

  • geocrunch

    CBallou,
    Thanks for posting this article, it has really helped in my project. However I did find few mistakes, mostly involving the user of ‘$gt’, ‘$lte’,… variables – they seem to be inverted in your example and thus don’t work. Once I inverted them back – no problem. Also using ‘_id’ doesn’t substitute the ObjectID – you would have to use the MongoID PHP class for that.
    One more thing, but a big one – a little trick I have come up with:
    Since $_SESSION variable is simply an array there is no need to store it in a serialized fashion as it does so by default inside ‘read/write’ functions. The original purpose of serializing the session in PHP was implemented for more common storage types, such as files and RDBMS databases, but in Mongo there is no need for this. So inside the ‘write’ function instead of inserting the serialized data as in ‘data’ => $data, you could insert the actual $_SESSION array as in ‘data’ => $_SESSION and inside the ‘read’ function instead of returning the string, ‘return $result[‘data’], you simply assign $_SESSION to the $result[‘data’] – $_SESSION = (array)$result[‘data’] and you don’t have to return anything. Doing this allows you to quote and manipulate the session collection and every individual $_SESSION variables inside the database. I did this in my project and it worked like a charm!

    • http://www.jqueryin.com Corey Ballou

      Hey geo, I appreciate all of the feedback. I had made a number of modifications due to noticing bugs in production but have been so busy that I didn’t get a chance to update the github code. In regards to the _id field, it will be replaced by a field aptly named sid or session_id. For the purposes of speed, this field will be indexed in the init() method.

      Your solution for manipulating the session data directly in Mongo is an interesting one. Although it strays from the standard session implementations, I believe the benefits might just outweigh any cons in this case. I will likely add this to the next revision which includes a number of added features from some of the github forks (I’ve been slacking).

  • Max

    Thank you for class but it have some errors (some of it may be of misconfiguration of my software).

    1. In method read() operator
    $expiry = time() + (int) $this->_config[‘lifetime’];
    must be:
    $expiry = time();
    Or session will live just one second.

    2. In method write() I cannot set “_id” in $new_obj – this update does not save for some reason.

    3. Static properties like MongoSession::SAFE cause error “Class ‘MongoSession’ not found”.

    4. Colon instead of dollar does not work in:
    $query = array(‘expiry’ => array(‘:lt’ => time()));

    5. Method gc() updates expired session instead of remove. Why?

  • http://www.securitywonks.net Raghu Veer

    I am interested to know if, mongosession can be comfortably used to replace replicated memcached servers ( http://repcached.lab.klab.org/ ), when storing sessions on multiple groups of replicated mongo servers kind of?

    thank you