Home > Code Snippets, Tips > PHP Sessions in Memcache – Locking Issues

PHP Sessions in Memcache – Locking Issues

December 9th, 2011 Leave a comment Go to comments

Actually it is ages since I sat down to scribble something. Well this one could not be avoided. Hence here it is.

In one of our FTE projects, we had faced a complication that Memcached on one node was using 100% cpu and php-cgi was complainging that the same node was not permitting any more memcached connections. The configuration was as what all says, session.save_handler = memcache, session.save_path = “tcp://:11211,tcp://:11211,tcp://:11211″. It was giving jitters to the night support, that this used to happen at the worse time when most of the clients are using the application. And eventually that memcached needed to be restarted, kicking all users out and every one has to login back from the login page. Now during the past weeks it was so horrible that we marked a portion of the ramdisk from one least loaded nodes and used nfs to export this to all the nodes for a file based sessions store.

To explain things more, our application written using the extJS javascript framework wrapped in a custom php framework, was having ajax and frames, with php sessions. Means any one request will lock the sessions till that request is completed, so at some point in the course of work, there was some database deadlocks which might have loaded one or two php requests tieing down the sessions. Memcached is not designed to work like this, and locking the tcp:// would cause the system to block any further php requests to the memcached daemon.

Armed with all these informations, I tried to find a custom solution which may be blocks only the particular connection or to be explicit, the exact browser-php-session would be blocked, and would not affect any other sessions. I expanded a User Contributed Note from php maual session_set_save_handler to build my first version which is reproduced below.


<?php
 
class SessionSaveHandler {
    protected 
$mc;
 
    public function 
__construct() {
        
session_set_save_handler(
            array(
$this"open"),
            array(
$this"close"),
            array(
$this"read"),
            array(
$this"write"),
            array(
$this"destroy"),
            array(
$this"gc")
        );
    }
    
    public function 
open($savePath$sessionName) {
        
$this->mc = new Memcache();
        
$this->mc->connect('127.0.0.1'11211);
        return 
true;
    }
 
    public function 
close() {
        
$this->mc->close();
        return 
true;
    }
 
    public function 
read($id) {
        return 
$this->mc->get($id);
    }
 
    public function 
write($id$data) {
        return 
$this->mc->set($id$datafalseini_get('session.gc_maxlifetime'));
    }
 
    public function 
destroy($id) {
        return 
$this->mc->delete($id);
    }
 
    public function 
gc($maxlifetime) {
    }
    
}
 
$mcsess = new SessionSaveHandler();
session_start();
 


This version should not be used on production, and is provided here only for academic purposes. This can produce unpredicted results. Consider the following scenario.

* We have a page that starts with few data, asks for user login
* Login is ajax processed ** (1)
* On loging, we try to fetch the user preferences from the server ** (2)
The preferences are saved into the session
* Simulataneously a request is sent to the server to check for notifications ** (3)
Notification shown is saved into the session

Now ** (2) takes more time than ** (3) due to some reasons in the production, where as in the test environment the sequnce was okay due to the negligble latency, and low database content volume, as well as very low practical concurrent sessions. In production, a subsequent page will not see any information related to ** (3) in the session. Because, php will read the session on start of ** (2) and then write that back with the updated values when ** (2) completes processing, but ** (3) has already processed and updated the session. This is why php had a session locking. When using the sessions in file method, the locking will affect only the said session, and never affect any other session. Well though the current workaround is much high profile than the default one, still it uses the NFS as well as has the scalablitiy issues so, with the expense of some processor cycles, the above buggy code was extended to the following.


<?php
 
class SessionSaveHandler {
    protected 
$mc;
    protected 
$pid;
 
    public function 
__construct() {
        
session_set_save_handler(
            array(
$this"open"),
            array(
$this"close"),
            array(
$this"read"),
            array(
$this"write"),
            array(
$this"destroy"),
            array(
$this"gc")
        );
        
$this->pid uniqid("");
    }
    
    public function 
open($savePath$sessionName) {
        
$this->mc = new Memcache();
        
$this->mc->connect('127.0.0.1'11211);
        return 
true;
    }
 
    public function 
close() {
        
$this->mc->close();
        return 
true;
    }
 
    public function 
read($id) {
        while((
$locked $this->mc->get($id '-lck2'))){
            if(
$locked == $this->pid or $locked === false){
              break;
            }
            
usleep (10);
        }
        
$this->mc->set($id '-lck2'$this->pidfalseini_get('session.gc_maxlifetime'));
        return 
$this->mc->get($id);
    }
 
    public function 
write($id$data) {
        
$locked $this->mc->get($id '-lck2');
        if(
$locked !== $this->pid){
            return;
        }
        
$this->mc->set($id$datafalseini_get('session.gc_maxlifetime'));
        
$this->mc->set($id '-lck2'falsefalseini_get('session.gc_maxlifetime'));
        return 
true;
    }
 
    public function 
destroy($id) {
        return 
$this->mc->delete($id);
    }
 
    public function 
gc($maxlifetime) {
    }
    
}
 
$mcsess = new SessionSaveHandler();
session_start();
 


This one addresses the issue and scenario outlined above by just locking the concerned browser-session. Whereas it provides the scalability as provided by Memcached pool. Theoretically this should work, and to an extend a medium level testing was conducted but still I could not get this code out into production. Since the GA releases are very stringent and this should go in the post chrismas QA release only. Anyway this being a complete detached code, I am providing the second one for download. PHP Sessions in Memcache Non Blocking (1149)

Categories: Code Snippets, Tips Tags:
  1. Mickey
    January 6th, 2013 at 16:43 | #1

    Thanks a lot for the article.
    I think you are missing the code to delete the memcache entries that are used for locking.
    EG the delete function should contain another line of code:

    $this->mc->delete($id.’-lck2′);

    The locking worked for me only this way..

    Thanks,
    Mickey.

  1. October 7th, 2013 at 04:17 | #1

sixty one − = fifty three