Symfony2 Memcache session locking

In one of the previous posts we wrote about session reliability. Today we will talk about “locking session data”. This is another session reliability topic and we will look at the problems that may occur in Symfony2 and how to solve them.

Session locking

Session locking is when the web server thread acquires an exclusive lock on the session data to avoid concurrent access. Browsers use HTTP 1.1 keep-alive and would normally just use one open TCP connection and reuse that to get all dynamic content. When loading images (and other static content) the browser may decide to use multiple TCP connections (concurrent) to get the data as fast as possible. This also happens when using AJAX. This may (and will most likely) lead to different workers (threads) on the web server answering these concurrent requests concurrently.

Each of the requests may read the session data update and write it back. The last write wins, so some writes may get lost. This can be countered by applying session locking. The session lock will prevent race conditions from occurring and prevent any corrupted data appearing in the session. This can easily be understood by looking at the following two images.

session-access-without-lockingsession-access-with-locking

The left image shows concurrent requests without session locking and the right shows concurrent requests with session locking. This is very well described in this post by Andy Bakun. Note that the above images are also from that post. Reading the Andy Bakun post allows you to truly understand the session locking problem (and the performance problems that AJAX may cause).

Symfony2 sessions

In Symfony2 one would normally use the NativeFileSessionHandler, which will just use the default PHP session handler. This works flawless in most cases. PHP uses “flock” to acquire an exclusive lock on the local filesystem. But when you scale out and run a server farm with multiple web servers you cannot use the local filesystem. You might be using a shared (NFS) filesystem and run into problems with “flock” (see the Linux NFS FAQs). If you use the database you may run into performance problems, escpecially when applying locking.

This leaves Memcache or Redis as options for session storage. These are fast key/value stores that can be used for session storage. Unfortunately the Symfony2 session storage implementations for Memcache (in Symfony) and Redis (in phpredis) do not implement session locking. This potentially leads to problems, especially when relying on AJAX calls as explained above. Note that other frameworks (like CakePHP) also do not implement session locking when using Memcache as session storage. Edit: This post has inspired the guys from SncRedisBundle and this Symfony2 bundle now supports session locking, which is totally awesome!

Custom save handlers

One can write “Custom Save Handlers” as described by the Symfony2 documentation:

Custom handlers are those which completely replace PHP’s built in session save handlers by providing six callback functions which PHP calls internally at various points in the session workflow. Symfony2 HttpFoundation provides some by default and these can easily serve as examples if you wish to write your own. — Symfony2 documentation

But you should be careful, since the examples do not implement session locking.

LswMemcacheBundle to the rescue

At LeaseWeb we love (to use) Memcache. Therefore, we have built session locking into our LswMemcacheBundle. It actually implements acquiring a “spin lock” with the timeout set to PHP’s “max_execution_time” (defaults to 30 seconds). The spin lock tries to acquire the lock every 150 ms (configurable). It will also hold the lock for a maximum time of the PHP “max_execution_time”. By using Memcache’s built-in key expire mechanism, we can ensure the lock is not held indefinitely.

This (spin-lock) implementation is a port of the session locking code from the memcached PECL module (written in C). Our bundle enables locking by default. If you want, you can disable the locking by setting the “locking” configuration parameter to “false” as described in the documentation.

This session locking code was also ported to SncRedisBundle and submitted as PR #109. LswMemcacheBundle is open-source and can be found on our GitHub account:

https://github.com/LeaseWeb/LswMemcacheBundle

Disable Chrome HTML5 client-side form validation in CakePHP

Since CakePHP 2.0, HTML5 input types in forms are supported in the Form Helper. Since CakePHP 2.3.1, the required attribute is set to true so that all forms have client-side validation by default. This may cause problems for people using Google Chrome, since that browser has some eccentric HTML5 form validation behavior. Fortunately we can easily disable this validation for the entire application. Firstly, you have to add the “className” with value “CustomForm” to the options of your “Form” helper in your “AppController”, like this:

<?php
App::uses('Controller', 'Controller');

class AppController extends Controller {

  public $helpers = array('Html','Form'=>array('className'=>'CustomForm'),'Session');

  // ...
}

Now add the “CustomForm” helper which extends the FormHelper in “app/View/Helper/CustomFormHelper.php” with the following contents:

<?php
App::uses('FormHelper', 'View/Helper');

class CustomFormHelper extends FormHelper {

  public function create($model = null, $options = array()) {
    if (is_array($model) && empty($options)) {
      $options = $model;
      $model = null;
    }

    if (!isset($options['novalidate'])) {
      $options['novalidate'] = true;
    }
    return parent::create($model, $options);
  }

}

Great, now your customers can continue to use your application without any problems. To further analyze the conversion and validation problems in Google Chrome for HTML5 form fields, use the HTML5 number field test.

CakePHP flash error message logging

When your CakePHP application cannot handle a submitted form it shows a flash error message (normally a red bar) and some fields in red (validation errors). By default, CakePHP log exceptions and fatal errors to “app/tmp/logs/error.log”. Unfortunately flash error messages and validation errors are not logged by default. This can be explained since the errors are actually handled and can thus be considered “user errors” and not “application errors”. But when a customer needs assistance, you actually need both types of error logs. Note that the Apache access log will also log these requests, but normally this does not provide enough information. In this post we will show how you can add flash error message logging to your CakePHP application.

First we will show that normal error logging is pretty useful. For example, when you call the PHP function “nltobr()” while this function is actually called “nl2br()” it will be logged as expected:

$ cat app/tmp/logs/error.log
2013-06-09 21:54:44 Error: [FatalErrorException] Call to undefined function nltobr()

For flash error message logging we have created a data structure and a CakePHP component that allows you to make a nice overview like this:

runtime_list

Figure 1: List with all pages served with flash error messages

And when you actually view the details of the flash error message you see the following screen:
runtime
Figure 2: Detail page for the table, showing a flash error message

You can clearly see that somebody was trying to edit user 1. The error message was not very descriptive, while the validation errors were pretty clear about the missing data (password).

Instructions

The data structure used is a MySQL database table that can be created by executing the following SQL:

CREATE TABLE `runtime_errors` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `created` datetime NOT NULL,
 `ip_address` varchar(255) NOT NULL,
 `method` varchar(255) NOT NULL,
 `url` varchar(255) NOT NULL,
 `referer` varchar(255) NOT NULL,
 `redirect_url` varchar(255) NOT NULL,
 `data` text NOT NULL,
 `validation_errors` text NOT NULL,
 `flash_message` text NOT NULL,
 `user_id` int(11) DEFAULT NULL,
 PRIMARY KEY (`id`),
 KEY `user_id` (`user_id`),
 KEY `created` (`created`),
 CONSTRAINT `runtime_errors_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Now create the following model and save it to “app/Model/RuntimeError.php”:

<?php
App::uses('AppModel', 'Model/App');

class RuntimeError extends AppModel {
  public $displayField = 'url';
  public $belongsTo = array('User');
}

Add the following component code to “app/Controller/Component/ErrorLoggerComponent.php” to log the flash messages:

<?php
App::uses('Component','Controller');

class ErrorLoggerComponent extends Component
{
  public function beforeRedirect(&$controller, $url, $status, $exit) {
    $this->log($controller,$url);
    return parent::beforeRedirect($controller, $url, $status, $exit);
  }

  public function beforeRender(&$controller) {
    $this->log($controller);
    parent::beforeRender($controller);
  }

  public function log(&$controller,$redirectUrl=false) {
    $session = $controller->Session->read();
    $flash = isset($session['Message']['flash'])?$session['Message']['flash']:false;
    $success = isset($flash['params']['class'])?$flash['params']['class']=='success':false;
    if ($flash && $success==false) {
      $controller->User->RuntimeError->create();
      $data = array('RuntimeError'=>array());
      $data['RuntimeError']['method'] = $controller->request->method();
      $data['RuntimeError']['url'] = $controller->request->here;
      $data['RuntimeError']['referer'] = $controller->request->referer();
      $data['RuntimeError']['redirect_url'] = $redirectUrl?Router::url($redirectUrl):'';
      $data['RuntimeError']['data'] = Spyc::YAMLDump($controller->params->data,2,80);
      $data['RuntimeError']['validation_errors'] = Spyc::YAMLDump($this->getErrors(),2,80);
      $data['RuntimeError']['flash_message'] = Spyc::YAMLDump($flash,2,80);
      $data['RuntimeError']['user_id'] = $controller->Auth->user('id');
      $data['RuntimeError']['ip_address'] = $controller->request->clientIp(true);
      if (!$controller->User->RuntimeError->save($data)) {
        throw new InternalErrorException('Could not log action');
      }
    }
  }

  private function getErrors()
  { $validationErrors = array();
    $models = ClassRegistry::keys();
    foreach ($models as $currentModel) {
      $currentObject = ClassRegistry::getObject($currentModel);
      if (is_a($currentObject, 'Model')) {
        if ($currentObject->validationErrors) {
          $validationErrors[$currentObject->alias] =& $currentObject->validationErrors;
        }
      }
    }
    return $validationErrors;
  }
}

Note that you may want to fine-tune the business logic that decides which messages to log (only errors), because the above code will log all flash messages without exception. To load the above component you have to add the “ErrorLogger” to the “$components” array in “app/Controller/AppController.php”:

 public $components = array(
   ...
   'ErrorLogger'=>array()
 );

Dependencies

For writing YAML output from CakePHP we have added “Spyc” to composer.json, which makes our full composer.json file look like this:

{
  "minimum-stability": "dev",
  "config": {
    "vendor-dir": "vendors"
  },
  "repositories" : [
    {
      "type": "package",
      "package": {
        "name" : "cakephp/cakephp",
        "version" : "2.3.5",
        "source" : {
          "type" : "git",
          "url" : "git://github.com/cakephp/cakephp.git",
          "reference" : "2.3.5"
        },
        "bin" : ["lib/Cake/Console/cake"]
      }
    }
  ],
  "extra": {
    "installer-paths": {
      "app/Plugin/DebugKit": ["cakephp/debug_kit"]
    }
  },
  "require" : {
    "php": ">=5.2",
    "cakephp/cakephp" : "2.3.5",
    "cakephp/debug_kit": "2.2.*",
    "mustangostang/spyc": "0.5.*"
  }
}

To achieve the nice looking index and view pages from Figure 1 and 2 you will have to “bake” some views and controllers. Good luck!

CakePHP and Composer

At LeaseWeb we are very fond of Composer, a package/dependency manager for PHP. It allows for easy software upgrades, with proper dependency management and provides an easy workflow which allows us to leave the dependencies out of the repository. This dramatically reduces the size of the repository. It also prevents programmers to fix framework issues without upstreaming the bugfixes. This is important, because patches on the framework have a high risk of complicating (security) upgrades later on.

We use Composer daily for Symfony2, which has excellent support for Composer (from version 2.1). CakePHP, another great PHP MVC framework, has recently released version 2.3.1, which is still lacking support for Composer. Fortunately it is easy to start using Composer in you CakePHP application. This guide will tell you how.

A typical CakePHP application has at least two directories at the top level: “app” and “lib”. The “app” directory contains the application, while the “lib” directory contains the CakePHP software. Composer will install CakePHP into the “vendors” directory, so we will have to change the location of CakePHP, fortunately this is very easy (see step 5, 6 and 7).

Step 0

Make a backup! So that you can revert if you are not happy with the result.

Step 1

Remove the lib directory, execute:

rm -Rf ./lib

Step 2

Get composer, execute:

curl -s https://getcomposer.org/installer | php

Step 3

Add the following composer.json file at the top level:

    {
	    "minimum-stability": "dev",
	    "config": {
	        "vendor-dir": "vendors"
	    },
	    "repositories" : [
		    {
			    "type": "package",
			    "package": {
				    "name" : "cakephp/cakephp",
				    "version" : "2.3.5",
				    "source" : {
					    "type" : "git",
					    "url" : "git://github.com/cakephp/cakephp.git",
					    "reference" : "2.3.5"
				    },
				    "bin" : ["lib/Cake/Console/cake"]
			    }
		    }
	    ],
	    "extra": {
		    "installer-paths": {
			    "app/Plugin/DebugKit": ["cakephp/debug_kit"]
		    }
	    },
	    "require" : {
		    "php": ">=5.2",
		    "cakephp/cakephp" : "2.3.5",
		    "cakephp/debug_kit": "2.2.*"
	    }
    }

Step 4

Install CakePHP and all its dependencies:

php composer.phar update

Step 5

If you have properly pointed your Apache webroot to “/app/webroot” you have to search this line in “app/webroot/index.php” and “app/webroot/test.php”:

define('CAKE_CORE_INCLUDE_PATH', ROOT . DS . 'lib');

And change it into:

define('CAKE_CORE_INCLUDE_PATH', ROOT . DS . 'vendors'.DS.'cakephp'.DS.'cakephp'.DS.'lib');

Step 6

If you use the root directory of the project as your Apache DocumentRoot (not recommended) you should also search and replace the same line in the top level “index.php”

Step 7

To repair the Cake Console you need to change to following line in “app/Console/cake.php”:

$root = dirname(dirname(dirname(__FILE__)));

And change it into:

$root = dirname(dirname(dirname(__FILE__))).$ds.'vendors'.$ds.'cakephp'.$ds.'cakephp';

Step 8 (optional)

As Miles Johnson writes on his blog how you can use Composer’s autoloader. All you need to do is add the following line to top of “app/Config/bootstrap.php”:

require_once dirname(dirname(__DIR__)) . '/vendors/autoload.php';