Posts Tagged ‘function’

Reliable AJAX: Timeout and Retry XMLHttpRequest

A lot of AJAX seems to rely on ideal conditions: a server that is both running and in good health, a reliable high speed internet connection, a lack of packet loss etc. Unfortunately, this is not always the case – what happens when your user triggers an event and the daemon is restarting or they are hopping access points/cell towers? What happens if the server load is sky high and only half of the requests are getting through? AJAX isn’t a very robust technology but we can increase the odds of our XMLHttpRequest transactions succeeding with some rudimentary JavaScript.

Newer Msxml XMLHTTP implementations support runtime-definable connection time-outs but that covers at best half of your audience and is therefore irrelevant to our needs. We will be implementing our own cross-browser compatible time-outs with the setTimeout() function. Once the time-out expires we will kill the XHR and repeat the process again, with a slightly longer time-out. The process is repeated as many times as necessary; each time increasing the duration. This is called backoff – similar in concept to but much less complicated than the exponential backoff algorithm Ethernet uses to recover from packet collisions.

The tricky part is to set the initial time-out to a value which gives the browser as long as reasonably possible to send and receive the request under strained conditions. Depending on how long the round trip takes in ideal circumstances a safe value can range anywhere from (plus) less than one second to several seconds – this is going to be unique to your application, resources and network so test thoroughly. If the time-out is too low we will be doing more harm than good by stopping and re-sending the request before it has even had a chance to complete. It might be sensible for you to give the last attempt a huge time-out.

The other major factor to consider is whether “the user knows best” in the given situation or not. If you are giving a visual cue to the user that there is AJAX activity happening in the background or if they expect to see something on the page update once the request has completed they might notice that things are taking a little long to react and re-trigger the event (read: hammer on the button like a mindless idiot). If you don’t want them to interfere with the time-out and re-send procedure take that into account when setting up your trigger – the example we are going to use assumes the user knows best. If they hammer on the button the best case scenario is their request goes through faster than if they left it alone and the worst case scenario is once they stop hammering the time-out and re-load cycle will run its course from the top anyway.

// Initialize global variables
var attempts = 0;
var timeout = 1000;    // In miliseconds
var max_attempts = 5;  // Six from 0
var script_url = ''    // The location of the target processing script
var junk = '';         // The variables we're POSTing, probably set in trigger()
var t = '';

// Cross-Browser XMLHttpRequest()
function createXHR()
{
	try { return new XMLHttpRequest(); } catch(e) {}
	try { return new ActiveXObject("Msxml2.XMLHTTP.7.0"); } catch (e) {}
	try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); } catch (e) {}
	try { return new ActiveXObject("Msxml2.XMLHTTP.5.0"); } catch (e) {}
	try { return new ActiveXObject("Msxml2.XMLHTTP.4.0"); } catch (e) {}
	try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); } catch (e) {}
	try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e) {}
	try { return new ActiveXObject("Microsoft.XMLHTTP"); } catch (e) {}
	alert("Please enable ActiveX or update your browser.");
	return null;
}

// User calls this function, i.e. onclick event handler
function trigger()
{	
	// Re-set the time-out in case the user interrupts the cycle
	clearTimeout(t);
	attempts = 0;
	timeout = 1000;

	driver();
}

// Handle the state changes of the XHR
function callback()
{
	if (xmlhttp.readyState==4 && xmlhttp.status==200)
	{
		// Success! Re-set the time-out for the next round.
		clearTimeout(t);
		attempts = 0;
		timeout = 1000;

		xmlhttp = '';

		// Perform whatever action we need to do on success.
	}
	else
	{
		// Error handling/Request status indication
	}
}

// The guts of the request
function driver()
{
	xmlhttp = createXHR();

	xmlhttp.open("POST",script_url,true);

	t = setTimeout(function(){timedout();}, timeout);
	xmlhttp.onreadystatechange=function(){callback();}

	xmlhttp.setRequestHeader("Content-type","application/x-www-form-urlencoded");
	xmlhttp.send(junk);
}

// Run this if we time out
function timedout()
{
	if(typeof(xmlhttp) == 'object')
		xmlhttp.abort();

	if(attempts < max_attempts)
	{
		// We timed out. Back off the time-out a bit.
		attempts++;
		timeout = timeout + 200;
		// We're adding 200ms to the timeout each iteration, YMMV.
		// Maybe if attempts == max_attempts make the timeout really huge.

		// Play it again, Sam!
		driver();
	}
	else
	{
		// We failed. Set the timeout back to default for the next round.
		timeout = 1000;
		attempts = 0;
		xmlhttp = '';
		alert("Timed out while sending request to server.");
	}
}

SHM/tmpfs File-Based PHP Cache/Datastore in RAM

It seems like only yesterday XCache was my knight in shining armour but a burst of segfaults has prompted the creation of a backup plan.

UPDATE It turns out my problem was actually PHP’s fault. Put that armour back on!

We can use files on tmpfs to provide much the same function as XCache or APC’s shared datastore. Start by mounting a slice somewhere appropriate (this line is for fstab):

none                    /mnt/ram        tmpfs           defaults,noatime,size=256M      0 0

Next we’ll create some basic interface functions. Let $config['fcache_path'] be the path to your tmpfs mount or writeable directory:

function fcache_isset($key)
{
	global $config;
	return @file_exists($config['fcache_path'].$key);
}

function fcache_unset($key)
{
	global $config;
	return @unlink($config['fcache_path'].$key);
}

function fcache_get($key)
{
	global $config;
	$val = @file_get_contents($config['fcache_path'].$key);
	if(empty($val))
		return NULL;
	else
		return $val;
}

function fcache_set($key, $val='')
{
	global $config;
	if(!empty($val))
	{
		$tmp = tempnam($config['fcache_path'], $key);
		if(@file_put_contents($tmp, $val))
		{
			if(@rename($tmp, $config['fcache_path'].$key))
				return true;
			else
				return false;
		}
		else
		{
			return false;
		}
	}
	return true;
}

I use rename() instead of flock() to make atomic writes because according to the manual page:

On some operating systems flock() is implemented at the process level. When using a multithreaded server API like ISAPI you may not be able to rely on flock() to protect files against other PHP scripts running in parallel threads of the same server instance!

They mention IIS’ ISAPI specifically but I’ve had enough problems with Apache’s mpm_worker lately that I’m not willing to take the risk. Further, I’d rather have the query run twice than have any lock-related hangups.

Now that we have some very basic functions to interface with we can put them to work in something useful. The following is what I’ve whipped up to switch between XCache, this file-based cache and no cache at all when pulling standard mysql results. cache_set() could easily be replaced with cache_unset() preceeding every update query but I do things this way to make the code more readable to me. You can also increase performance by using only arrays instead of converting between arrays and objects but this software was written entirely using mysql_fetch_object() and the caching was an afterthought.

Let $config['cache'] contain the cache type.

function cache_get($key, $query)
{
	global $config;
	if($config['cache'] == 'xcache' and function_exists('xcache_get'))
	{
		$serialized = xcache_get($key);
		if($serialized != NULL)
		{
			$unserialized = unserialize($serialized);
			$object = (object) $unserialized;
			return $object;
		}
		else
		{
			$result = mysql_query($query);
			if($result === false)
				return false;
			if(mysql_num_rows($result) > 0)
			{
				$object = mysql_fetch_object($result);
				$array = (array) $object;
				$serialized = serialize($array);
				xcache_set($key, $serialized);
				return $object;
			}
			else
			{
				return true;
			}
		}
	}
	elseif($config['cache'] == 'fcache' and function_exists('fcache_get'))
	{
		$serialized = fcache_get($key);
		if($serialized != NULL)
		{
			$unserialized = unserialize($serialized);
			$object = (object) $unserialized;
			return $object;
		}
		else
		{
			$result = mysql_query($query);
			if($result === false)
				return false;
			if(mysql_num_rows($result) > 0)
			{
				$object = mysql_fetch_object($result);
				$array = (array) $object;
				$serialized = serialize($array);
				fcache_set($key, $serialized);
				return $object;
			}
			else
			{
				return true;
			}
		}
	}
	else
	{
		$result = mysql_query($query);
		if($result === false)
			return false;
		if(mysql_num_rows($result) > 0)
		{
			$object = mysql_fetch_object($result);
			return $object;
		}
		else
		{
			return true;
		}
	}
}

function cache_set($key, $query)
{
	global $config;
	if($config['cache'] == 'xcache' and function_exists('xcache_unset'))
	{
		$result = mysql_query($query);
		if($result === false)
			return false;
		xcache_unset($key);
		return true;
	}
	elseif($config['cache'] == 'fcache' and function_exists('fcache_unset'))
	{
		$result = mysql_query($query);
		if($result === false)
			return false;
		fcache_unset($key);
		return true;
	}
	else
	{
		$result = mysql_query($query);
		if($result === false)
			return false;
		return true;
	}
}

function cache_unset($key)
{
	global $config;
	if($config['cache'] == 'xcache' and function_exists('xcache_unset'))
	{
		xcache_unset($key);
		return true;
	}
	elseif($config['cache'] == 'fcache' and function_exists('fcache_unset'))
	{
		fcache_unset($key);
		return true;
	}
	else
	{
		return true;
	}
}

Please note that cache_get() checks if the returned value is NULL, it does NOT use (x|f)cache_isset() because that would introduce a serious race condition.

This implementation leaves out two important features that xcache has: garbage collection and timeouts. Garbage collection can be handled by a cron script and use of the find command to take out stale entries. Timeouts can be implemented by inserting a value into the file and comparing it against the file’s time stamp and the current time – a clever idea I got from looking over http://flourishlib.com/docs/fCache.

Will Bond’s fCache is probably what you’re looking for if you want to be able to port between all of the major datastores easily and have individual control over an item’s expiration. However, this implementation uses a rand()om number for garbage collection and may be subject to the race (or minor hangup depending on how file_put_contents() handles locking) condition we avoid here with atomic writes.

Here are some completely meaningless apache bench benchmarks against an AJAX app’s polling script on a live, production server:

Without datastore

Document Length:        105 bytes

Concurrency Level:      20
Time taken for tests:   122.880 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      2420000 bytes
Total POSTed:           2290229
HTML transferred:       1050000 bytes
Requests per second:    81.38 [#/sec] (mean)
Time per request:       245.761 [ms] (mean)
Time per request:       12.288 [ms] (mean, across all concurrent requests)  
Transfer rate:          19.23 [Kbytes/sec] received          
         18.20 kb/s sent       
         37.43 kb/s total      
 
Connection Times (ms)          
              min  mean[+/-sd] median   max   
Connect:       91  202 341.7    154    9170   
Processing:    19   43  40.3     32     746   
Waiting:       19   40  39.0     31     746   
Total:        114  245 344.1    194    9193   
 
Percentage of the requests served within a certain time (ms) 
  50%    194    
  66%    212    
  75%    224    
  80%    232    
  90%    264    
  95%    408    
  98%    480    
  99%   3170    
 100%   9193 (longest request)

XCache datastore

Document Length:        105 bytes

Concurrency Level:      20
Time taken for tests:   121.803 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      2420000 bytes
Total POSTed:           2290229
HTML transferred:       1050000 bytes
Requests per second:    82.10 [#/sec] (mean)
Time per request:       243.605 [ms] (mean)
Time per request:       12.180 [ms] (mean, across all concurrent requests)
Transfer rate:          19.40 [Kbytes/sec] received
                        18.36 kb/s sent
                        37.76 kb/s total

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       91  201 331.6    154    3418
Processing:    19   42  40.6     32     798
Waiting:       19   39  39.2     31     788
Total:        115  243 334.1    193    3459

Percentage of the requests served within a certain time (ms)
  50%    193
  66%    210
  75%    221
  80%    228
  90%    260
  95%    405
  98%    473
  99%   3163
 100%   3459 (longest request)

fcache

Document Length:        105 bytes

Concurrency Level:      20
Time taken for tests:   121.174 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      2420000 bytes
Total POSTed:           2291374
HTML transferred:       1050000 bytes
Requests per second:    82.53 [#/sec] (mean)
Time per request:       242.347 [ms] (mean)
Time per request:       12.117 [ms] (mean, across all concurrent requests)
Transfer rate:          19.50 [Kbytes/sec] received
                        18.47 kb/s sent
                        37.97 kb/s total

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       90  199 320.8    154    3407
Processing:    19   42  38.7     33     747
Waiting:       19   40  37.3     32     747
Total:        116  242 323.2    193    3486

Percentage of the requests served within a certain time (ms)
  50%    193
  66%    210
  75%    222
  80%    231
  90%    262
  95%    408
  98%    475
  99%   3161
 100%   3486 (longest request)

Interesting to see fcache narrowly beat out xcache but since the testing environment is not perfectly controlled the results are of course useless.

PHP with Calls to ffmpeg Functions Terminate Early

If your PHP scripts die suddenly on calls to ffmpeg functions you may have:
- Built a new version of PHP without recompiling ffmpeg-php against it
- Switched to or from thread safe PHP

The solution in both cases is:

# emerge --update --newuse ffmpeg ffmpeg-php
Return top
foxpa.ws
Online Marketing Toplist
Internet
Technology Blogs - Blog Rankings

Internet Blogs - BlogCatalog Blog Directory

Technology blogs
Bad Karma Networks

Please Donate!


Made in Canada  •  There's a fox in the Gibson!  •  2010-12