Zend - The PHP Company




Miscellaneous

Add Code


Database session storage with locking  

Type: code fragment
Added by: erwinmoller
Entered: 31/10/2005
Last modified: 02/12/2005
Rating: - (fewer than 3 votes)
Views: 3252
If you have been looking for a databasesolution using locking of the session for the duration of the script, and couldn't find any, here is one.


[Database sessionstorage, with locking to prevent simultanious access to the session]

Hi all,

I have been looking for a databasesolution using locking of the session for the duration of the script, and couldn't find any, so here is my own.

I appologize for the long posting beforehand, but the script contains a lot of comments that explain what happens.
Since this is my first (but tested) version, you might want to use the debugging-logic.

Some notes:
- The script also contain a testscript that produces a frameset with a few php-pages that simply increase a counter.
I used them to test the concurrent sessionaccess-functionality/blocking

- I use a database abstractionlayer named ADODB.
(http://www.phplens.com/adodb)

- My code is developed for M$ SQL Server 7
(Sorry for that. ;-) But the databaseabstractionlayer should make it easy for you to switch.

- Errorlogging: Since the sessionhandler will never show its errors, I made a seperate function to log things.
In my case I log them into a seperate database, but you could as well try a flat file approach. (I needed a seperate database because I use a transaction that is something rolled back, thus also rolling back my log-comments.)
Just call session_log(<somestringhere>). As you will see, I commented them all out in this example.

- I also set a few ini-parameters, not all are relevant here.

- The reason I wrote this in the first place is that I needed to get the moment a session is destroyed.
This is done in: MySession_onDestroy($aSession)
It uses a function posted by bmorel that REALLY unserializes a session. (Thanks morel!)

- require_once("dbconnect.php");
You will find a line where this file is required.
The file follows, and you will see it includes itself adodb/adodb.inc.php
That is the databaseabstractionlayer I use, if you want to use the following literal code, be sure to get it first.

- ABOUT THE FOLLOWING FILES:
* The mainfile is called ini_settings.php
* It includes a file I called db_connect.php
* You should get ADODB database abstractionlayer software (NOT included here)
If you want you can do without, but you should rewrite all databasecalls according to your database.
* A file called: sessiontest_concurrency.php
This file contains 10 times sessiontest_concurrency_frame.php
* A file called: sessiontest_concurrency_frame.php
This file actually uses a session to increase a counter in $_SESSION["counter"]
It also sleeps for 1 second



db_connect.php
*******************************************
<?
// This script creates a DB-connection handler
// It should be included in ini_settings.php

require_once('adodb/adodb.inc.php');

$server "localhost";
$dbusername "XXX";
$dbpassword "YYY";
$dbdatabasename "YourDatabaseNameHere";

// Change the following line to suit your needs: This is for SQL Server
$connection = &ADONewConnection('odbc_mssql');
$dsn "Driver={SQL Server};Server=$server;Database=$dbdatabasename;";
$connection->Connect($dsn,$dbusername,$dbpassword);

// force ASSOC
$connection->SetFetchMode(ADODB_FETCH_ASSOC);
?>
*******************************************


- Now follows the mainscript, it is called ini_setting.php in my case.
This script expects a table named tblsession in the database, that looks as follows:

-----------------------------
tblsession:
session_id varchar(50) Primary Key,
session_lastupdated datetime NOT NULL,
session_data text,
session_lock CHAR(1)
-----------------------------


ini_settings.php
******************************************
<?
// Coded by Erwin Moller, Darwine. erwin@darwineX.nl (remove X)

// ini_settings.php
// This file should be included above EVERY PHP script!
// Please note this script also includes a databaseconnectionfile.

// What is in this file?
// 1) php.ini settings (in case they are wrong, this file will overwrite them anyway)
// 2) a databaseconnection include file. (Just included, not here)
// 3) userdefined Session-handler (Now we can trigger the sessiontimeout event)
// 4) This file also starts the session.

// This file sets a lot of ini-settings.
// In case somebody overrides php.ini with different values, we define here what we expect.
// Fail if not possible!, by checking ini_get(). If fail -> error and stop executing
// based on listings in: http://nl2.php.net/manual/en/ini.core.php

// ***********************************************
// IncidentenDB_SessionTimeOut in seconds
// How many seconds of inactivity before session is not valid anymore?
$MySession_SessionTimeOut 3600// 60*60 is one hour.


 // 1) php.ini settings (in case they are wrong, this file will overwrite them anyway)

$iniSettings = array (
    
"short_open_tag" => "1",
    
// always on "track_vars" => "On",
    
"arg_separator.output" => "&",
    
"arg_separator.input" => "&",
    
"register_globals" => "0",

    
// magic quotes on
    
"magic_quotes_gpc" => "1",
    
"magic_quotes_runtime" => "0",

    
"register_argc_argv" => "1",

        
// email stuff
    
"SMTP" => "xs4all.nl",
    
"smtp_port" => "25",

    
// Includepath!
    // Make sure this points to the place where all includes reside, also adodb/etc
    
"include_path" => ".;C:InetpubwwwrootMyProjectincludes",

    
"default_mimetype" => "text/html",

    
// errorhandling
    
"error_reporting" => E_ALL,

    
// sessions
    
"session.save_handler" => "user",
    
"session.auto_start" => "0"
);


foreach (
$iniSettings as $inikey => $inivalue){
    
// echo "setting $inikey to $inivalue<br> Old value = ".ini_get($inikey)."<br>";
    // ini_set("magic_quotes_gpc", "1");
    
ini_set($inikey$inivalue);

    
// and test
    
if (ini_get($inikey) != $inivalue){
        echo 
"UNRECOVERABLE INI-PROBLEM IN ini_settings.php.<br>";
        echo 
"CANNOT SET $inikey to $inivalue.<br><h2>EXITING</h2>";
        exit;
    }
}

// END ini-settings





// 2) userdefined Session-handler (So we can trigger the sessiontimeout event)
// Session logic overwrite
// Loosely based on http://www.zend.com/zend/spotlight/code-gallery-wade8.php
// But we include the lockinglogic.

// First check if we have a valid databaseconnection.
require_once("dbconnect.php");
if (!isset(
$connection)){
    echo 
"<b>Problem in ini_settings.php: No databaseconnection found.</b><br><h2>FAILING</h2>";
    exit;
}




// Change the save_handler to use our sessionhandlers
session_set_save_handler (
            
'MySession_open',
            
'MySession_close',
            
'MySession_read',
            
'MySession_write',
            
'MySession_destroy',
            
'MySession_gc');

/* Start the session */
session_start();
header("Cache-control: private");

// MySession_open
function MySession_open($path$name) {
  
// session_log ("In MySession_open $path=$path, $name=$name ... Garbagecollection MySession_gc()");
  
MySession_gc();
  return 
TRUE;
}

// MySession_close()
function MySession_close() {
    
// session_log ("In MySession_close.");
    
return TRUE;
}

// MySession_readRead session data from database
function MySession_read($ses_id) {
    global 
$connection;
    
// session_log ("In MySession_read $ses_id=$ses_id. microtime=".microtime(true));

    // check if session has a lock already.
    // If so, wait and retry

    
$sessionLockedWait true;
    
$session_thedata "";

    while (
$sessionLockedWait){
        
$connection->StartTrans();
        
// lock the row
        
$connection->RowLock("tblsession","session_id=$ses_id");
        
// session_log ("[MySession_read] Just row-locked tblsession on session_id=$ses_id for the duration of the transaction.");


        
$SQL_session "SELECT session_lock, session_data FROM tblsession WHERE (session_id='".$ses_id."');";
        
// session_log ("$SQL_session");
        // session_log ("Query is: $SQL_session=$SQL_session");
        
$RS_session $connection->Execute($SQL_session);
        
// Nothing found? Continue
        // session_log ("DEBUG: recordcount is: ".$RS_session->RecordCount());

        
if ($RS_session->RecordCount() == 0){
            
$sessionLockedWait false;
            
// NEW SESSION: INSERT A NEW SESSIONRECORD WITH session_lock='Y'

            
$newsession = array();
            
$newsession["session_id"] = $ses_id;
            
$newsession["session_lastupdated"] = $connection->DBTimeStamp(time());
            
$newsession["session_data"] = '';
            
$newsession["session_lock"] = 'Y';
            
$newsessionupdatetable 'tblsession';

            
$SQL_session_new $connection->GetInsertSQL(&$newsessionupdatetable$newsessiontrue);

            
// session_log ("CREATING A NEW SESSIONRECORD WITH: $SQL_session_new ");
            
$session_thedata "";
            
$newcreateresult $connection->Execute($SQL_session_new);
            
// session_log ("Session inserted. Result=".(($newcreateresult) ? "true": "false"));
        
} else {
            
// something found? If session_lock = 'N', continue
            // session_log ("Something found. $RS_session->Fields("session_lock")= ".$RS_session->Fields("session_lock"));
            
if ($RS_session->Fields("session_lock") == 'N') {
                
$sessionLockedWait false;
                
// EXISTING SESSION: UPDATE WITH session_lock='Y' AND FRESH session_lastupdated
                
$updatesession = array();
                
$updatesession["session_lock"] = 'Y';
                
$updatesession["session_lastupdated"] = $connection->DBTimeStamp(time());

                
// GetUpdateSQL(&$rs, $arrFields, $forceUpdate=false,$magicq=false, $force=null)
                
$SQL_session_update_lock $connection->GetUpdateSQL(&$RS_session$updatesessiontruetrue);
                
// session_log ("UPDATING AN EXISTING SESSIONLOCK WITH: $SQL_session_update_lock");
                
$connection->Execute($SQL_session_update_lock);

                
// Now retrieve session_data
                
$session_thedata $RS_session->Fields("session_data");
            } else {
                
// session still locked, leave $sessionLockedWait = true;
            
}
        }

        
// wait?
        
if ($sessionLockedWait){
          
// try again in 0.1 second
          // session_log ("sessionLockedWait is still true. We fail the transaction.");
          
$connection->FailTrans();
        }
        
// session_log ("calling completetrans.");
        
$connection->CompleteTrans();

        
// wait?
        
if ($sessionLockedWait){
          
// try again in 0.1 second
          // session_log ("sessionLockedWait is still true. We sleep 0.1 secs before trying again.");
          
usleep(100000);
        }
    }

    
// return $session_thedata
    
return $session_thedata;
}

/* Write new data to database */
function MySession_write($ses_id$data) {
  global 
$connection;
  
// write the data away and UNLOCK the session (set session_lock = 'N').
  // We know a session exists now, since we created it in MySession_read().
  // So we do not check for INSERT or UPDATE. Always UPDATE
  
$SQL_session_update "UPDATE tblsession SET ";
  
$SQL_session_update .= " session_lastupdated =  ".$connection->DBTimeStamp(time()).",";
  
$SQL_session_update .= " session_lock = 'N', ";
  
$SQL_session_update .= " session_data = ".$connection->qstr($data);
  
$SQL_session_update .= " WHERE (session_id = '".$ses_id."'); ";

  
// session_log ("UPDATING AN EXISTING SESSIONLOCK WITH: $SQL_session_update");
  
$connection->Execute($SQL_session_update);

  
// session_log ("In MySession_write $ses_id=$ses_id en &data=$data. Using SQL: $SQL_session_update");
  
return TRUE;
}


/* Destroy session record in database */
function MySession_destroy($ses_id) {
    global 
$connection;

    
// Before deleting, get the Session, and pass it to the function: MySession_onDestroy($theoldsession);
    
$SQL_session_data "SELECT session_data FROM tblsession WHERE (session_id='".$ses_id."');";
    
// session_log ("[MySession_destroy] Query is: $SQL_session_data=$SQL_session_data");
    
$RS_session_data $connection->Execute($SQL_session_data);

    
$someSession $RS_session_data->Fields("session_data");
    
// Possible last action on the session
    
MySession_onDestroy($someSession);

    
$SQL_session_delete "DELETE FROM tblsession WHERE (session_id = '".$ses_id."')";
    
// session_log ("In MySession_destroy(). Deleting sessionrecord with SQL: $SQL_session_delete");
    
$connection->Execute($SQL_session_delete);
    return 
TRUE;
}


/* Garbage collection, deletes old sessions */
function MySession_gc() {
  
// This function uses MySession_SessionTimeOut to decide which sessions are not valid anymore.
  // Strategy: find all session_id's in tblsession and pass them one-by-one to the MySession_destroy() function
  
global $MySession_SessionTimeOut;
  global 
$connection;

  
// session_log ("In MySession_gc, the garbage collector. $MySession_SessionTimeOut = $MySession_SessionTimeOut seconds.");

  
$SQL_session_gc "SELECT session_id FROM tblsession WHERE ";
  
$SQL_session_gc .= " (session_lastupdated < ".$connection->DBTimeStamp(time()-$MySession_SessionTimeOut).");";

  
// session_log ("Query is: $SQL_session_gc=$SQL_session_gc");
  
$RS_session_gc $connection->Execute($SQL_session_gc);
  if (
$RS_session_gc->RecordCount() > 0){
    
// delete them all.
    
foreach($RS_session_gc as $k => $row) {
        
MySession_destroy($row["session_id"]);
    }
  }
  return 
TRUE;
}




function 
MySession_onDestroy($aSession) {
    
// This function receives a SERIALIZED session that is about to be destroyed.
    //echo ("[MySession_onDestroy]: Received a still serialized session: ".htmlentities($aSession));
    
$theSession session_real_decode($aSession);

    
// Now you have the session as you are used to.
    // Access the elements 'normally'. For example: $theSession["userid"] now, instead of $_SESSION["userid"]
    // So this is the place to do what you want with the session. After this routine it will be destroyed.
}



// The following function was made by bmorel
// It is used as posted on: http://nl3.php.net/manual/en/function.session-decode.php

define('PS_DELIMITER''|');
define('PS_UNDEF_MARKER''!');

function 
session_real_decode($str)
{
   
$str = (string)$str;

   
$endptr strlen($str);
   
$p 0;

   
$serialized '';
   
$items 0;
   
$level 0;

   while (
$p $endptr) {
       
$q $p;
       while (
$str[$q] != PS_DELIMITER)
           if (++
$q >= $endptr) break 2;

       if (
$str[$p] == PS_UNDEF_MARKER) {
           
$p++;
           
$has_value false;
       } else {
           
$has_value true;
       }

       
$name substr($str$p$q $p);
       
$q++;

       
$serialized .= 's:' strlen($name) . ':"' $name '";';

       if (
$has_value) {
           for (;;) {
               
$p $q;
               switch (
$str[$q]) {
                   case 
'N'/* null */
                   
case 'b'/* boolean */
                   
case 'i'/* integer */
                   
case 'd'/* decimal */
                       
do $q++;
                       while ( (
$q $endptr) && ($str[$q] != ';') );
                       
$q++;
                       
$serialized .= substr($str$p$q $p);
                       if (
$level == 0) break 2;
                       break;
                   case 
'R'/* reference  */
                       
$q+= 2;
                       for (
$id ''; ($q $endptr) && ($str[$q] != ';'); $q++) $id .= $str[$q];
                       
$q++;
                       
$serialized .= 'R:' . ($id 1) . ';'/* increment pointer because of outer array */
                       
if ($level == 0) break 2;
                       break;
                   case 
's'/* string */
                       
$q+=2;
                       for (
$length=''; ($q $endptr) && ($str[$q] != ':'); $q++) $length .= $str[$q];
                       
$q+=2;
                       
$q+= (int)$length 2;
                       
$serialized .= substr($str$p$q $p);
                       if (
$level == 0) break 2;
                       break;
                   case 
'a'/* array */
                   
case 'O'/* object */
                       
do $q++;
                       while ( (
$q $endptr) && ($str[$q] != '{') );
                       
$q++;
                       
$level++;
                       
$serialized .= substr($str$p$q $p);
                       break;
                   case 
'}'/* end of array|object */
                       
$q++;
                       
$serialized .= substr($str$p$q $p);
                       if (--
$level == 0) break 2;
                       break;
                   default:
                       return 
false;
               }
           }
       } else {
           
$serialized .= 'N;';
           
$q+= 2;
       }
       
$items++;
       
$p $q;
   }
   return @
unserialize'a:' $items ':{' $serialized '}' );
}



function 
session_log ($someString){
// need fresh connection because the old one is rollback sometimes. :P
// This function is very rude, but I only used it during debugging.
// Make a table that has a field name 'remark' of type TEXT or varchar(1000) or something.
$logserver "localhost";
$logdbusername "XXXX";
$logdbpassword "XXXX";
$logdbdatabasename "mySessionLoggerDB";

$someString "[ ".$_GET["i"]."] [".microtime(true)."]".$someString;

$LOGconnection = &ADONewConnection('odbc_mssql');
$logdsn "Driver={SQL Server};Server=$logserver;Database=$logdbdatabasename;";
$LOGconnection->Connect($logdsn,$logdbusername,$logdbpassword);
// Carefull here, I am lazy and just change ' into '' which is enough for SQLServer.
// Replace this loggingfunction with something more elaborate.
$LOGSQL "INSERT INTO tblsessionlog (remark) VALUES ('".str_replace("'","''",$someString)."')";
$result $LOGconnection->Execute($LOGSQL);
if (!
$result){
    
// Hope this gets on the users screen....
  
echo "<hr><h2>ERROR LOGGING THIS: $someString </h2>SQL: $LOGSQL<hr>";
}
}

?>

******************************************






sessiontest_concurrency.php
*******************************************
<?
    $numberOfFrames 
10;
    
$starThingy str_repeat("*,",$numberOfFrames) ;
    
$starThingy substr($starThingy,0,strlen($starThingy)-1);
?>
<html>
 <frameset border=1 rows="<?= $starThingy ?>">
<?
    
// pass a i=... with each request: easy for debugging later.
 
for ($i=0;$i<$numberOfFrames;$i++){
?>
  <frame src="sessiontest_concurrency_frame.php?i=<?= $i ?>&microtime=<?= microtime(true?>">
<?
}
?>
 </frameset>
</html>

*******************************************


sessiontest_concurrency_frame.php
*******************************************
<?
    
include '../includes/ini_settings.php';
        
// spend some time to mimic scriptduration
        
sleep(1);

if (isset(
$_SESSION["counter"])){
    
$_SESSION["counter"] = ((int)$_SESSION["counter"] + 1);
}else {
    
$_SESSION["counter"] = 1;
}
?>
<html>
 <head>
  <title>Sessionconcurrencytest</title>
 </head>
<body>
This is sessiontest_concurrency_frame.php [Sessionid= <?= session_id() ?>]
<br>
counter is: <?= $_SESSION["counter"?>
<br>
backtrace for debugging: i in GET is for this request: <?= $_GET["i"?>
</body>
</html>
*******************************************


Disclaimer:
I hope this can help you going with sessionhandlers and database.
I tested it many times, and couldn't get an error or simultanious access to the same sessionrecord.
That doesn't mean it is good code, that just means it seems to work for me.
This solution comes with no warranty and such, use at your risk, I am nor liable, etc etc.

If anybody wants to use this solution or want to develop your own solution based on this one, be my guest: no strings attached. :-)

Regards,
Erwin Moller
(remove the X to email me)


Usage Example




Rate This Script





Search



This Category All Categories