I have written a simple session_set_save_handler using a MySQL innoDB table as the storage backend. MySQL is 5.5.
Code looks like so (very simplified, but hopefully illustrates the flow of code), I am using RedBean ORM for interacting with the database, but the commands should be clear in showing what was being written, read or deleted.:
class MySession{
private $_database;
public function __construct($database){
//database object is injected using dependency injection then assigned to private var
//Hook up handlers
session_set_save_handler(
array($this, "open"),
array($this, "close"),
array($this, "read"),
array($this, "write"),
array($this, "destroy"),
array($this, "garbageCollection"));
}
//Register the shutdown function
register_shutdown_function('session_write_close');
//start
session_start();
//Regenerate session id
//(NOTE: in production code, the id is regenerated every 10 minutes,
//but regenerating allows us to trigger the race condition for demonstration.
session_regenerate_id(TRUE);
public function open($savePath, $sessionName){
$this->_database->exec('SET TRANSACTION ISOLATION LEVEL SERIALIZABLE');
$this->_database->begin(); //Begin Transaction
return true;
}
public function close(){
$this->_database->commit(); //Commit the transaction
return true;
}
public function read($id){
$this->_database->exec('SELECT * FROM session WHERE identifier = ? LOCK IN SHARE MODE', array($id));
$session = $this->_database->findOne('session', 'identifier = ?', array($id));
return $session->data;
}
public function write($id, $sessionData){
$this->_database->exec('SELECT * FROM session WHERE identifier = ? FOR UPDATE', array($id));
$session = $this->_database->findOne('session', 'identifier = ?', array($id));
//We need to create a new session
if ($session == NULL){
$session = $this->_database->dispense('session');
}
$this->_database->store($session);
return TRUE;
}
public function destroy($id){
$this->_database->exec('SELECT * FROM session WHERE identifier = ? FOR UPDATE', array($id));
$session = $this->_database->findOne('session', 'identifier = ?', array($id));
if ($session != NULL){
$this->_database->trash($session);
}
return TRUE;
}
public function garbageCollection($maxlifetime){
$old = ... //Calculate the time for expired sessions
$this->_database->exec("DELETE from session WHERE last_activity < ?", array($old));
return TRUE;
}
}
The problem is that even with the locking in place, if I send multiple requests to the application (for example, using AJAX), the race condition still occurs and the data stored in the session is lost. I have pinned it down to session_regenerate_id(TRUE)
, which deletes the previous session to being the cause of the issue. However, even with the row locking, the issue still occurs.
I have looked at this page, as well as some code from that author, but that uses MyISAM tables which implements table locking.
Can anyone shed some light as to why the locking is not making a difference and perhaps offer some solutions to this problem?