Thursday, September 12, 2013

Re: CakePHP ComboSession serious looping bug in documentation

This is resolved and patched in the documentation: https://github.com/cakephp/docs/commit/23fc03c12c3eea8456e895c2696444e037cdcc56

Thanks!
/Lionel

On Thursday, 12 September 2013 10:23:21 UTC+8, Lionel Chan wrote:
Hi All

Environment:
CakePHP version: 2.4.0 stable
latest PHP 5.3

We just recently pushed our app to AWS Beanstalk with ElastiCache + RDS as our combo session, and using the ComboSession technique described in here. At first everything was ok, but then out of sudden, our app keep running into some infinity loops that we can't really reproduce at that point of time. What we can do is basically remove the cookie key from RDS, or clear browser cookie. Clearly it has something to do with Session.

So these are our sample error_logs:

[Wed Sep 11 03:56:27 2013] [error] [client 10.251.91.107] PHP Fatal error:  Maximum execution time of 60 seconds exceeded in /var/app/current/lib/Cake/Model/Datasource/CakeSession.php on line 612
[Wed Sep 11 04:00:49 2013] [error] [client 10.251.91.107] PHP Fatal error:  Maximum execution time of 60 seconds exceeded in /var/app/current/lib/Cake/Model/Datasource/CakeSession.php on line 612
[Wed Sep 11 04:03:02 2013] [error] [client 10.251.91.107] PHP Fatal error:  Maximum execution time of 60 seconds exceeded in /var/app/current/lib/Cake/Model/Datasource/CakeSession.php on line 612
.... repeating ....
[Thu Sep 12 01:11:48 2013] [error] [client 10.251.91.107] PHP Fatal error:  Maximum execution time of 60 seconds exceeded in /var/app/current/lib/Cake/Model/Datasource/CakeSession.php on line 612
[Thu Sep 12 01:13:50 2013] [error] [client 10.251.91.107] PHP Fatal error:  Maximum execution time of 60 seconds exceeded in /var/app/current/lib/Cake/Model/Datasource/CakeSession.php on line 612
[Thu Sep 12 01:15:15 2013] [error] [client 10.251.91.107] PHP Fatal error:  Allowed memory size of 268435456 bytes exhausted (tried to allocate 523800 bytes) in /var/app/current/lib/Cake/Model/Model.php on line 1368

So apparently CakeSession is trying to session_start in a loop that it can't resolve the session successfully.

Then we found out one serious issue with the ComboSession.php mentioned in the post http://book.cakephp.org/2.0/en/development/sessions.html#creating-a-custom-session-handler, that, let's say, if Cache lost the session data, and Cache::delete($id) fails, parent::destroy() will simply won't be called to destroy expired session.

That says, we run into this kind of loop:

Let's assume Cache lost the session data, and RDS retained a piece of expired session data:
1. CakeSession::start()
2. CakeSession::_startSession() - session data get loaded from RDS because Cache::read returns false
3. CakeSession::_checkValid()
4. CakeSession::_validAgentAndTime() - obviously timeout, we got an invalid data here
5. CakeSession::destroy() - this calls destroy the session and trying to call session_destroy()
6. ComboSession::destroy($id) - this calls attempted to destroy the session data from Cache but failed (Memcache engine says not found!), and since it's failed, parent::destroy() will never get triggered, and so old session data is survived.
7. Go to step 1 and continues looping, and PHP will dies either the whole memory get filled up or time out.


To reproduce, we use this modified version of ComboSession to simulate the problem: (The following code is to demonstrate the bug, not a working copy!)

<?php
App::uses('DatabaseSession', 'Model/Datasource/Session');

class ComboSession extends DatabaseSession implements CakeSessionHandlerInterface {
    public $cacheKey;

    public function __construct() {
        $this->cacheKey = Configure::read('Session.handler.cache');
        parent::__construct();
    }

    // read data from the session.
    public function read($id) {
        $result = false; //simulate cache read fails
        if ($result) {
            return $result;
        }
        return parent::read($id);
    }

// write data into the session.
    public function write($id, $data) {
        $result = Cache::write($id, $data, $this->cacheKey);
        if ($result) {
            return parent::write($id, $data);
        }
        return false;
    }

    // destroy a session.
    public function destroy($id) {
        $result = false; //simulate Cache::delete fails
        if ($result) {
            return parent::destroy($id);
        }
        return false;
    }

    // removes expired sessions.
    public function gc($expires = null) {
        return Cache::gc($this->cacheKey) && parent::gc($expires);
    }
}

And in the database, try modify your current session with time (in data), and expires to a earlier time. You should run into infinity loop in the next run.

-----------------------------------

In the end, we fix it by issue delete on both Cache and RDS session data regardless if Cache succeed or not:

    public function destroy($id) {
//Delete, regardless of cache succeed or not (so to prevent infinity loop)
        Cache::delete($id, $this->cacheKey);
        return parent::destroy($id);
    }

Suggestion: To prevent someone from having this issue again, I think it's best if you can update your documentation at http://book.cakephp.org/2.0/en/development/sessions.html#creating-a-custom-session-handler.

Thanks!
/Lionel 

--
Like Us on FaceBook https://www.facebook.com/CakePHP
Find us on Twitter http://twitter.com/CakePHP
 
---
You received this message because you are subscribed to the Google Groups "CakePHP" group.
To unsubscribe from this group and stop receiving emails from it, send an email to cake-php+unsubscribe@googlegroups.com.
To post to this group, send email to cake-php@googlegroups.com.
Visit this group at http://groups.google.com/group/cake-php.
For more options, visit https://groups.google.com/groups/opt_out.

No comments: