Parallelise http transfers for trip planner tester
Parallelise http transfers for trip planner tester

--- a/labs/tripPlannerTester.kml.php
+++ b/labs/tripPlannerTester.kml.php
@@ -8,6 +8,50 @@
 	}
 	else {
 		return (($pBegin - $pEnd) * (1 - ($pStep / $pMax))) + $pEnd;
+	}
+}
+require ("../lib/rolling-curl/RollingCurl.php");
+function processResult_cb($response, $info, $request)
+{
+	global $testRegions, $regionTimes,$csv,$kml, $latdeltasize,$londeltasize;
+	$md = $request->metadata;
+	$tripplan = json_decode($response);
+	$plans = Array();
+	//var_dump(Array($info, $request));
+	if (is_array($tripplan->plan->itineraries->itinerary)) {
+		foreach ($tripplan->plan->itineraries->itinerary as $itineraryNumber => $itinerary) {
+			$plans[floor($itinerary->duration / 60000) ] = $itinerary;
+		}
+	}
+	else {
+		$plans[floor($tripplan->plan->itineraries->itinerary->duration / 60000) ] = $tripplan->plan->itineraries->itinerary;
+	}
+	if ($csv) echo "{$md['i']},{$md['j']}," . min(array_keys($plans)) . ",$latdeltasize, $londeltasize,{$md['key']}\n";
+	if ($kml) {
+		$time = min(array_keys($plans));
+		$plan = "";
+		if (is_array($plans[min(array_keys($plans)) ]->legs->leg)) {
+			foreach ($plans[min(array_keys($plans)) ]->legs->leg as $legNumber => $leg) {
+				$plan.= processLeg($legNumber, $leg) . ",";
+			}
+		}
+		else {
+			$plan.= processLeg(0, $plans[min(array_keys($plans)) ]->legs->leg);
+		}
+		if (isset($tripplan->error) && $tripplan->error->id == 404) {
+			$time = 999;
+			$plan = "Trip not possible without excessive walking from nearest bus stop";
+		}
+		$testRegions[] = Array(
+			"lat" => $md['i'],
+			"lon" => $md['j'],
+			"time" => $time,
+			"latdeltasize" => $latdeltasize,
+			"londeltasize" => $londeltasize,
+			"regionname" => $md['key'],
+			"plan" => $plan . "<br/><a href='" . htmlspecialchars($url) . "'>original plan</a>"
+		);
+		$regionTimes[] = $time;
 	}
 }
 function Gradient($HexFrom, $HexTo, $ColorSteps)
@@ -49,18 +93,17 @@
 		//}
 		//$walkingstep.= floor($step->distance) . "m";
 		//return $walkingstep;
+		
 	}
 }
 $csv = false;
 $kml = true;
 if ($kml) {
-	//header('Content-Type: application/vnd.google-earth.kml+xml');
+	header('Content-Type: application/vnd.google-earth.kml+xml');
 	echo '<?xml version="1.0" encoding="UTF-8"?>
 <kml xmlns="http://www.opengis.net/kml/2.2"><Document>';
 }
 include ('../include/common.inc.php');
-//Test code to grab transit times
-// make sure to sleep(10);
 $boundingBoxes = Array(
 	"belconnen" => Array(
 		"startlat" => - 35.1928,
@@ -105,72 +148,29 @@
 $useragent = "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.1) Gecko/20061204 Firefox/2.0.0.1";
 if ($csv) echo "<pre>";
 if ($csv) echo "lat,lon,time,latdeltasize, londeltasize, region key name\n";
+$rc = new RollingCurl("processResult_cb");
+$rc->window_size = 3;
 foreach ($boundingBoxes as $key => $boundingBox) {
 	for ($i = $boundingBox['startlat']; $i >= $boundingBox['finishlat']; $i-= $latdeltasize) {
 		for ($j = $boundingBox['startlon']; $j <= $boundingBox['finishlon']; $j+= $londeltasize) {
 			$url = $otpAPIurl . "ws/plan?date=" . urlencode($startDate) . "&time=" . urlencode($startTime) . "&mode=TRANSIT%2CWALK&optimize=QUICK&maxWalkDistance=440&wheelchair=false&toPlace=" . $i . "," . $j . "&fromPlace=$fromPlace";
-			$ch = curl_init($url);
-			curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
-			curl_setopt($ch, CURLOPT_HEADER, 0);
-			curl_setopt($ch, CURLOPT_HTTPHEADER, array(
+			$request = new RollingCurlRequest($url);
+			$request->headers = Array(
 				"Accept: application/json"
-			));
-			curl_setopt($ch, CURLOPT_TIMEOUT, 5);
-			$page = curl_exec($ch);
-			if (curl_errno($ch)) {
-				if ($csv) echo "Trip planner temporarily unavailable: " . curl_errno($ch) . " " . curl_error($ch);
-			}
-			else {
-				$tripplan = json_decode($page); 
-				$plans = Array();
-				if (is_array($tripplan->plan->itineraries->itinerary)) {
-					foreach ($tripplan->plan->itineraries->itinerary as $itineraryNumber => $itinerary) {
-						$plans[floor($itinerary->duration / 60000) ] = $itinerary;
-					}
-				}
-				else {
-					$plans[floor($tripplan->plan->itineraries->itinerary->duration / 60000) ] = $tripplan->plan->itineraries->itinerary;
-				}
-				if ($csv) echo "$i,$j," . min(array_keys($plans)) . ",$latdeltasize, $londeltasize,$key\n";
-				if ($kml) {
-					$time = min(array_keys($plans));
-					$plan = "";
-					if (is_array($plans[min(array_keys($plans)) ]->legs->leg)) {
-						foreach ($plans[min(array_keys($plans)) ]->legs->leg as $legNumber => $leg) {
-							$plan .= processLeg($legNumber, $leg).",";
-						}
-					}
-					else {
-						$plan .= processLeg(0, $plans[min(array_keys($plans)) ]->legs->leg);
-					}
-						if (isset($tripplan->error) && $tripplan->error->id == 404) {
-							$time = 999;
-							$plan = "Trip not possible without excessive walking from nearest bus stop";
-						}
-					$testRegions[] = Array(
-						"lat" => $i,
-						"lon" => $j,
-						"time" => $time,
-						"latdeltasize" => $latdeltasize,
-						"londeltasize" => $londeltasize,
-						"regionname" => $key,
-						"plan" => $plan . "<br/><a href='". htmlspecialchars($url)."'>original plan</a>"
-					);
-					$regionTimes[] = $time;
-				}
-			}
-			flush(); @ob_flush();
-			curl_close($ch);
-		}
-	}
-}
+			);
+			$request->metadata = Array( "i" => $i, "j" => $j, "key" => $key);
+			$rc->add($request);
+		}
+	}
+}
+$rc->execute();
 if ($kml) {
 	$colorSteps = 9;
 	//$minTime = min($regionTimes);
 	//$maxTime = max($regionTimes);
 	//$rangeTime = $maxTime - $minTime;
 	//$deltaTime = $rangeTime / $colorSteps;
-	$Gradients = Gradient(strrev("66FF00"), strrev("FF0000"), $colorSteps); // KML is BGR not RGB so strrev
+	$Gradients = Gradient(strrev("66FF00") , strrev("FF0000") , $colorSteps); // KML is BGR not RGB so strrev
 	foreach ($testRegions as $testRegion) {
 		//$band = (floor(($testRegion[time] - $minTime) / $deltaTime));
 		$band = (floor($testRegion[time] / 10));

--- /dev/null
+++ b/lib/rolling-curl/.svn/all-wcprops
@@ -1,1 +1,42 @@
+K 25
+svn:wc:ra_dav:version-url
+V 22
+/svn/!svn/ver/20/trunk
+END
+RollingCurlGroup.php
+K 25
+svn:wc:ra_dav:version-url
+V 43
+/svn/!svn/ver/20/trunk/RollingCurlGroup.php
+END
+example_groups.php
+K 25
+svn:wc:ra_dav:version-url
+V 41
+/svn/!svn/ver/20/trunk/example_groups.php
+END
+example.php
+K 25
+svn:wc:ra_dav:version-url
+V 34
+/svn/!svn/ver/20/trunk/example.php
+END
+RollingCurl.php
+K 25
+svn:wc:ra_dav:version-url
+V 38
+/svn/!svn/ver/20/trunk/RollingCurl.php
+END
+CHANGELOG.txt
+K 25
+svn:wc:ra_dav:version-url
+V 36
+/svn/!svn/ver/20/trunk/CHANGELOG.txt
+END
+README.txt
+K 25
+svn:wc:ra_dav:version-url
+V 33
+/svn/!svn/ver/20/trunk/README.txt
+END
 

--- /dev/null
+++ b/lib/rolling-curl/.svn/entries
@@ -1,1 +1,233 @@
-
+10
+
+dir
+20
+http://rolling-curl.googlecode.com/svn/trunk
+http://rolling-curl.googlecode.com/svn
+
+
+
+2010-09-12T20:39:22.711474Z
+20
+alexander.makarow
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+74aa2acc-2e27-11de-b2a4-4f96ceaaac44
+
+RollingCurlGroup.php
+file
+
+
+
+
+2011-04-10T08:32:48.081650Z
+73c08d9e9e24b4adc89816624c7aca30
+2010-09-12T20:39:22.711474Z
+20
+alexander.makarow
+has-props
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+5152
+
+example_groups.php
+file
+
+
+
+
+2011-04-10T08:32:48.082650Z
+907ed82a47d346c39acbd5578e1d0230
+2010-09-12T20:39:22.711474Z
+20
+alexander.makarow
+has-props
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+1367
+
+example.php
+file
+
+
+
+
+2011-04-10T08:32:48.083650Z
+87aa845abfaffc09ed4eca024f2a8b8a
+2010-09-12T20:39:22.711474Z
+20
+alexander.makarow
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+1860
+
+RollingCurl.php
+file
+
+
+
+
+2011-04-10T08:32:48.084650Z
+205391c449f3f3ee050004dadc374dc8
+2010-09-12T20:39:22.711474Z
+20
+alexander.makarow
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+10444
+
+CHANGELOG.txt
+file
+
+
+
+
+2011-04-10T08:32:48.085650Z
+d0452f6f9530ed04580159121d0fd5f7
+2010-09-12T20:39:22.711474Z
+20
+alexander.makarow
+has-props
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+662
+
+README.txt
+file
+
+
+
+
+2011-04-10T08:32:48.085650Z
+60dd357081431c0f2b82989cdbce8615
+2010-09-12T20:39:22.711474Z
+20
+alexander.makarow
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+6355
+
+

--- /dev/null
+++ b/lib/rolling-curl/.svn/prop-base/CHANGELOG.txt.svn-base
@@ -1,1 +1,6 @@
+K 13
+svn:eol-style
+V 6
+native
+END
 

--- /dev/null
+++ b/lib/rolling-curl/.svn/prop-base/RollingCurlGroup.php.svn-base
@@ -1,1 +1,6 @@
+K 13
+svn:eol-style
+V 6
+native
+END
 

--- /dev/null
+++ b/lib/rolling-curl/.svn/prop-base/example_groups.php.svn-base
@@ -1,1 +1,6 @@
+K 13
+svn:eol-style
+V 6
+native
+END
 

--- /dev/null
+++ b/lib/rolling-curl/.svn/text-base/CHANGELOG.txt.svn-base
@@ -1,1 +1,15 @@
+Rolling Curl changelog
+======================
 
+September 13, 2010
+------------------
+- Bug #12, #14: Fixed default options overriding (LionsAd)
+- Bug #10: Added use of curl_multi_select to avoid burning CPU (LionsAd)
+- Enh #6, #9: Added $request as parameter to callback function (LionsAd)
+- Chg: Request renamed to RollingCurlRequest (LionsAd)
+- Added RollingCurlGroup class that allows processing groups of requests (LionsAd)
+- More cleanup at unsetting a class (LionsAd)
+- Timeout parameter for curl_multi_select is now configurable (LionsAd)
+- single_curl now returns true (LionsAd)
+- Readme corrections (Alexander Makarov)
+- Code cleanup (Alexander Makarov)

--- /dev/null
+++ b/lib/rolling-curl/.svn/text-base/README.txt.svn-base
@@ -1,1 +1,210 @@
-
+Rolling Curl
+============
+
+RollingCurl allows you to process multiple HTTP requests in parallel using CURL PHP library.
+
+Released under the Apache License 2.0.
+
+Authors
+-------
+- Was originally written by [Josh Fraser](joshfraser.com).
+- Currently maintained by [Alexander Makarov](http://rmcreative.ru/).
+- Received significant updates and patched from [LionsAd](http://github.com/LionsAd/rolling-curl).
+
+Overview
+--------
+RollingCurl is a more efficient implementation of curl_multi() curl_multi is a great way to process multiple HTTP requests in parallel in PHP.
+curl_multi is particularly handy when working with large data sets (like fetching thousands of RSS feeds at one time). Unfortunately there is
+very little documentation on the best way to implement curl_multi. As a result, most of the examples around the web are either inefficient or
+fail entirely when asked to handle more than a few hundred requests.
+
+The problem is that most implementations of curl_multi wait for each set of requests to complete before processing them. If there are too many requests
+to process at once, they usually get broken into groups that are then processed one at a time. The problem with this is that each group has to wait for
+the slowest request to download. In a group of 100 requests, all it takes is one slow one to delay the processing of 99 others. The larger the number of
+requests you are dealing with, the more noticeable this latency becomes.
+
+The solution is to process each request as soon as it completes. This eliminates the wasted CPU cycles from busy waiting. Also there is a queue of
+cURL requests to allow for maximum throughput. Each time a request is completed, a new one is added from the queue. By dynamically adding and removing
+links, we keep a constant number of links downloading at all times. This gives us a way to throttle the amount of simultaneous requests we are sending.
+The result is a faster and more efficient way of processing large quantities of cURL requests in parallel.
+
+Callbacks
+---------
+
+Each of requests usually do have a callback to process results that is being executed when request is done
+(both successfully or not).
+
+Callback accepts three parameters and can look like the following one:
+~~~
+[php]
+function request_callback($response, $info, $request){
+    // doing something with the data received
+}
+~~~
+
+- $response contains received page body.
+- $info is an associative array that holds various information about response such as HTTP response code, content type,
+time taken to make request etc.
+- $request contains RollingCurlRequest that was used to make request.
+
+Examples
+--------
+### Hello world
+
+~~~
+[php]
+// an array of URL's to fetch
+$urls = array("http://www.google.com",
+              "http://www.facebook.com",
+              "http://www.yahoo.com");
+
+// a function that will process the returned responses
+function request_callback($response, $info, $request) {
+	// parse the page title out of the returned HTML
+	if (preg_match("~<title>(.*?)</title>~i", $response, $out)) {
+		$title = $out[1];
+	}
+	echo "<b>$title</b><br />";
+	print_r($info);
+	echo "<hr>";
+}
+
+// create a new RollingCurl object and pass it the name of your custom callback function
+$rc = new RollingCurl("request_callback");
+// the window size determines how many simultaneous requests to allow.
+$rc->window_size = 20;
+foreach ($urls as $url) {
+    // add each request to the RollingCurl object
+    $request = new RollingCurlRequest($url);
+    $rc->add($request);
+}
+$rc->execute();
+~~~
+
+
+### Setting custom options
+
+Set custom options for EVERY request:
+
+~~~
+[php]
+$rc = new RollingCurl("request_callback");
+$rc->options = array(CURLOPT_HEADER => true, CURLOPT_NOBODY => true);
+$rc->execute();
+~~~
+
+Set custom options for A SINGLE request:
+
+~~~
+[php]
+$rc = new RollingCurl("request_callback");
+$request = new RollingCurlRequest($url);
+$request->options = array(CURLOPT_HEADER => true, CURLOPT_NOBODY => true);
+$rc->add($request);
+$rc->execute();
+~~~
+
+### Shortcuts
+
+~~~
+[php]
+$rc = new RollingCurl("request_callback");
+$rc->get("http://www.google.com");
+$rc->get("http://www.yahoo.com");
+$rc->execute();
+~~~
+
+### Class callbacks
+
+~~~
+[php]
+class MyInfoCollector {
+    private $rc;
+
+    function __construct(){
+        $this->rc = new RollingCurl(array($this, 'processPage'));
+    }
+
+    function processPage($response, $info, $request){
+      //...
+    }
+
+    function run($urls){
+        foreach ($urls as $url){
+            $request = new RollingCurlRequest($url);
+            $this->rc->add($request);
+        }
+        $this->rc->execute();
+    }
+}
+
+$collector = new MyInfoCollector();
+$collector->run(array(
+    'http://google.com/',
+    'http://yahoo.com/'
+));
+~~~
+
+### Using RollingCurlGroup
+
+~~~
+[php]
+class TestCurlRequest extends RollingCurlGroupRequest {
+    public $test_verbose = true;
+
+    function process($output, $info) {
+        echo "Processing " . $this->url . "\n";
+        if ($this->test_verbose)
+            print_r($info);
+
+        parent::process($output, $info);
+    }
+}
+
+class TestCurlGroup extends RollingCurlGroup {
+    function process($output, $info, $request) {
+        echo "Group CB: Progress " . $this->name . " (" . ($this->finished_requests + 1) . "/" . $this->num_requests . ")\n";
+        parent::process($output, $info, $request);
+    }
+
+    function finished() {
+        echo "Group CB: Finished" . $this->name . "\n";
+        parent::finished();
+    }
+}
+
+$group = new TestCurlGroup("High");
+$group->add(new TestCurlRequest("www.google.de"));
+$group->add(new TestCurlRequest("www.yahoo.de"));
+$group->add(new TestCurlRequest("www.newyorktimes.com"));
+$reqs[] = $group;
+
+$group = new TestCurlGroup("Normal");
+$group->add(new TestCurlRequest("twitter.com"));
+$group->add(new TestCurlRequest("www.bing.com"));
+$group->add(new TestCurlRequest("m.facebook.com"));
+$reqs[] = $group;
+
+$reqs[] = new TestCurlRequest("www.kernel.org");
+
+// No callback here, as its done in Request class
+$rc = new GroupRollingCurl();
+
+foreach ($reqs as $req)
+$rc->add($req);
+
+$rc->execute();
+~~~
+
+The same function (add) can be used both for adding requests and groups of requests.
+The "callback" in request and groups is:
+
+process($output, $info)
+
+and
+
+process($output, $info, $request)
+
+Also you can override RollingCurlGroup::finished() that will be executed right after finishing group processing.
+
+$Id$

--- /dev/null
+++ b/lib/rolling-curl/.svn/text-base/RollingCurl.php.svn-base
@@ -1,1 +1,375 @@
-
+<?php
+/*
+Authored by Josh Fraser (www.joshfraser.com)
+Released under Apache License 2.0
+
+Maintained by Alexander Makarov, http://rmcreative.ru/
+
+$Id$
+*/
+
+/**
+ * Class that represent a single curl request
+ */
+class RollingCurlRequest {
+    public $url = false;
+    public $method = 'GET';
+    public $post_data = null;
+    public $headers = null;
+    public $options = null;
+
+    /**
+     * @param string $url
+     * @param string $method
+     * @param  $post_data
+     * @param  $headers
+     * @param  $options
+     * @return void
+     */
+    function __construct($url, $method = "GET", $post_data = null, $headers = null, $options = null) {
+        $this->url = $url;
+        $this->method = $method;
+        $this->post_data = $post_data;
+        $this->headers = $headers;
+        $this->options = $options;
+    }
+
+    /**
+     * @return void
+     */
+    public function __destruct() {
+        unset($this->url, $this->method, $this->post_data, $this->headers, $this->options);
+    }
+}
+
+/**
+ * RollingCurl custom exception
+ */
+class RollingCurlException extends Exception {
+}
+
+/**
+ * Class that holds a rolling queue of curl requests.
+ *
+ * @throws RollingCurlException
+ */
+class RollingCurl {
+    /**
+     * @var int
+     *
+     * Window size is the max number of simultaneous connections allowed.
+     *
+     * REMEMBER TO RESPECT THE SERVERS:
+     * Sending too many requests at one time can easily be perceived
+     * as a DOS attack. Increase this window_size if you are making requests
+     * to multiple servers or have permission from the receving server admins.
+     */
+    private $window_size = 5;
+
+    /**
+     * @var float
+     *
+     * Timeout is the timeout used for curl_multi_select.
+     */
+    private $timeout = 10;
+
+    /**
+     * @var string|array
+     *
+     * Callback function to be applied to each result.
+     */
+    private $callback;
+
+    /**
+     * @var array
+     *
+     * Set your base options that you want to be used with EVERY request.
+     */
+    protected $options = array(
+        CURLOPT_SSL_VERIFYPEER => 0,
+        CURLOPT_RETURNTRANSFER => 1,
+        CURLOPT_CONNECTTIMEOUT => 30,
+        CURLOPT_TIMEOUT => 30
+    );
+
+    /**
+     * @var array
+     */
+    private $headers = array();
+
+    /**
+     * @var Request[]
+     *
+     * The request queue
+     */
+    private $requests = array();
+
+    /**
+     * @var RequestMap[]
+     *
+     * Maps handles to request indexes
+     */
+    private $requestMap = array();
+
+    /**
+     * @param  $callback
+     * Callback function to be applied to each result.
+     *
+     * Can be specified as 'my_callback_function'
+     * or array($object, 'my_callback_method').
+     *
+     * Function should take three parameters: $response, $info, $request.
+     * $response is response body, $info is additional curl info.
+     * $request is the original request
+     *
+     * @return void
+     */
+    function __construct($callback = null) {
+        $this->callback = $callback;
+    }
+
+    /**
+     * @param string $name
+     * @return mixed
+     */
+    public function __get($name) {
+        return (isset($this->{$name})) ? $this->{$name} : null;
+    }
+
+    /**
+     * @param string $name
+     * @param mixed $value
+     * @return bool
+     */
+    public function __set($name, $value) {
+        // append the base options & headers
+        if ($name == "options" || $name == "headers") {
+            $this->{$name} = $value + $this->{$name};
+        } else {
+            $this->{$name} = $value;
+        }
+        return true;
+    }
+
+    /**
+     * Add a request to the request queue
+     *
+     * @param Request $request
+     * @return bool
+     */
+    public function add($request) {
+        $this->requests[] = $request;
+        return true;
+    }
+
+    /**
+     * Create new Request and add it to the request queue
+     *
+     * @param string $url
+     * @param string $method
+     * @param  $post_data
+     * @param  $headers
+     * @param  $options
+     * @return bool
+     */
+    public function request($url, $method = "GET", $post_data = null, $headers = null, $options = null) {
+        $this->requests[] = new RollingCurlRequest($url, $method, $post_data, $headers, $options);
+        return true;
+    }
+
+    /**
+     * Perform GET request
+     *
+     * @param string $url
+     * @param  $headers
+     * @param  $options
+     * @return bool
+     */
+    public function get($url, $headers = null, $options = null) {
+        return $this->request($url, "GET", null, $headers, $options);
+    }
+
+    /**
+     * Perform POST request
+     *
+     * @param string $url
+     * @param  $post_data
+     * @param  $headers
+     * @param  $options
+     * @return bool
+     */
+    public function post($url, $post_data = null, $headers = null, $options = null) {
+        return $this->request($url, "POST", $post_data, $headers, $options);
+    }
+
+    /**
+     * Execute processing
+     *
+     * @param int $window_size Max number of simultaneous connections
+     * @return string|bool
+     */
+    public function execute($window_size = null) {
+        // rolling curl window must always be greater than 1
+        if (sizeof($this->requests) == 1) {
+            return $this->single_curl();
+        } else {
+            // start the rolling curl. window_size is the max number of simultaneous connections
+            return $this->rolling_curl($window_size);
+        }
+    }
+
+    /**
+     * Performs a single curl request
+     *
+     * @access private
+     * @return string
+     */
+    private function single_curl() {
+        $ch = curl_init();
+        $request = array_shift($this->requests);
+        $options = $this->get_options($request);
+        curl_setopt_array($ch, $options);
+        $output = curl_exec($ch);
+        $info = curl_getinfo($ch);
+
+        // it's not neccesary to set a callback for one-off requests
+        if ($this->callback) {
+            $callback = $this->callback;
+            if (is_callable($this->callback)) {
+                call_user_func($callback, $output, $info, $request);
+            }
+        }
+        else
+            return $output;
+        return true;
+    }
+
+    /**
+     * Performs multiple curl requests
+     *
+     * @access private
+     * @throws RollingCurlException
+     * @param int $window_size Max number of simultaneous connections
+     * @return bool
+     */
+    private function rolling_curl($window_size = null) {
+        if ($window_size)
+            $this->window_size = $window_size;
+
+        // make sure the rolling window isn't greater than the # of urls
+        if (sizeof($this->requests) < $this->window_size)
+            $this->window_size = sizeof($this->requests);
+
+        if ($this->window_size < 2) {
+            throw new RollingCurlException("Window size must be greater than 1");
+        }
+
+        $master = curl_multi_init();
+
+        // start the first batch of requests
+        for ($i = 0; $i < $this->window_size; $i++) {
+            $ch = curl_init();
+
+            $options = $this->get_options($this->requests[$i]);
+
+            curl_setopt_array($ch, $options);
+            curl_multi_add_handle($master, $ch);
+
+            // Add to our request Maps
+            $key = (string) $ch;
+            $this->requestMap[$key] = $i;
+        }
+
+        do {
+            while (($execrun = curl_multi_exec($master, $running)) == CURLM_CALL_MULTI_PERFORM) ;
+            if ($execrun != CURLM_OK)
+                break;
+            // a request was just completed -- find out which one
+            while ($done = curl_multi_info_read($master)) {
+
+                // get the info and content returned on the request
+                $info = curl_getinfo($done['handle']);
+                $output = curl_multi_getcontent($done['handle']);
+
+                // send the return values to the callback function.
+                $callback = $this->callback;
+                if (is_callable($callback)) {