PHP, AJAX, and Race Conditions

We came across a pretty interesting race condition regarding AJAX requests and our PHP back end. A quick search will find a few articles about the problem; this one I think sums it up best. There are some comments there that are worth skimming over if you want more ideas. Unfortunately, nothing in there about a definitive solution.

To summarize briefly, asynchronous calls are made from the client web browser which are then processed by the server without making sure previous requests are completed. A lot of solutions seem to focus on the front end using Javascript to prevent this. Although it is a good practice to avoid making rapid requests of your server from the client, you will still need to address this issue on the back end. I am sure we can come up with ways to help mitigate this issue but eventually the back end must always be the final say in serving requests relying on nothing from the client.

/**
 * Create and release database locks to prevent concurrency issues. This is a rough
 * solution and you may want to look at try-catch, closing db connections, throwing
 * exceptions, etc.
 *
 * Also, keep in mind that using persistent connections isn't ideal and neither is
 * sleep. This isn't an ideal solution, IMHO.
 *
 * @author Michael Hradek
 */
final class DatabaseLock
{
    /**
	 * @var int Seconds before lock times out.
	 */
    const LOCK_TIMEOUT = 10;
    
    /**
     * @var int Retry limit count.
     */
    const LOCK_RETRY_LIMIT = 5;
    
    /**
 	 * Create a lock using MySQL. This is a wrapper for that functionality.
	 *
 	 * @param String
	 */
    public static function lock($lock_name)
    {
        $i = 0;
        do
        {
            if(self::isLockUsed($lock_name) === false)
            {
                $dbObj = /** Your DB obj instantiation. Persistant connection. */
                $sql = "SELECT GET_LOCK(?, ?)";
                $result = $dbObj->query($sql, $lock_name, self::LOCK_TIMEOUT);
                if($result && $result->getColumn(0) === 1) {
                    return true;
                }
            }
            usleep(1000); $i++;
        } while ($i < self::LOCK_RETRY_LIMIT);

        return false;
    }

    /**
 	 * Release an existing lock.
	 *
 	 * @param String
	 */
    public static function release($lock_name)
    {
        $dbObj = /** Your DB obj instantiation. */
        $sql = "SELECT RELEASE_LOCK(?)";
        $result = $dbObj->query($sql, $lock_name);
        
        if($result && $result->getColumn(0) === 1) {
            return true;
        }
        
        return false;
    }
    
	/**
	 * Checks to see if a lockname is in use. Calling GET_LOCK release previous
	 * lock if the lock name is identical.
	 *
	 * @param String
	 * @return bool
	 */
	private static function isLockUsed($lock_name)
	{
        $dbObj = /** Your DB obj instantiation. */
		$sql = "SELECT IS_USED_LOCK(?)";
		$result = $dbObj->query($sql, $lock_name);

		return ($result === false);
	}
}

The solution my colleagues and I came up with above requires use of MySQL’s get_lock and release_lock functionality. We spent some time thinking about Session flags/semaphores, horribly un-scalable sleeping, and semaphores via PHP itself. In the case of session flags we found that session was too slow to keep up with multiple incoming requests. We had between four to six to n requests come through and announce that they are starting the session lock. Wha?!

This solution requires InnoDB and a version of MySQL that allows locking. I believe that in conjunction with solid abuse monitoring on the web server side we create a solution that ensures served requests are not abusive and properly handled. Is it scalable? Better than say sleep but still can have problems. Any time we introduce code dealing with timing out we run the risk of trouble. Also, according to the MySQL documentation calling get_lock multiple times overrides the previous lock so you must check to see if its currently in use before calling it.

This entry was posted in Programming and tagged , . Bookmark the permalink.

Comments are closed.