Commit 29076983 authored by Bas de Nooijer's avatar Bas de Nooijer

Added loadbalancer plugin, including a unittest and example

parent c0bbb200
<?php
require('init.php');
htmlHeader();
// create a client instance and get loadbalancer plugin instance
$client = new Solarium_Client($config);
$loadbalancer = $client->getPlugin('loadbalancer');
// apply loadbalancer settings
$optionsSolrOne = array('host' => '127.0.0.1', 'port' => 8983);
$optionsSolrTwo = array('host' => '127.0.0.1', 'port' => 7574);
$loadbalancer->addServer('solr1', $optionsSolrOne, 100);
$loadbalancer->addServer('solr2', $optionsSolrTwo, 200);
$loadbalancer->addServer('solr3', $optionsSolrTwo, 1);
// create a basic query to execute
$query = $client->createSelect();
// execute the query multiple times, displaying the server for each execution
for($i=1; $i<=8; $i++) {
$resultset = $client->select($query);
echo 'Query execution #' . $i . '<br/>';
echo 'NumFound: ' . $resultset->getNumFound(). '<br/>';
echo 'Server: ' . $loadbalancer->getLastServerKey() .'<hr/>';
}
// force a server for a query (normally solr 3 is extremely unlikely based on it's weight)
$loadbalancer->forceServerForNextQuery('solr3');
$resultset = $client->select($query);
echo 'Query execution with server forced to solr3<br/>';
echo 'NumFound: ' . $resultset->getNumFound(). '<br/>';
echo 'Server: ' . $loadbalancer->getLastServerKey() .'<hr/>';
// test a ping query
$query = $client->createPing();
$client->ping($query);
echo 'Loadbalanced ping query, should display a loadbalancing server:<br/>';
echo 'Ping server: ' . $loadbalancer->getLastServerKey() .'<hr/>';
// exclude ping query from loadbalancing
$loadbalancer->addBlockedQueryType(Solarium_Client::QUERYTYPE_PING);
$client->ping($query);
echo 'Non-loadbalanced ping query, should not display a loadbalancing server:<br/>';
echo 'Ping server: ' . $loadbalancer->getLastServerKey() .'<hr/>';
htmlFooter();
\ No newline at end of file
......@@ -110,6 +110,11 @@
<li><a href="6.3-placeholder-syntax.php">6.3 Placeholder syntax</a></li>
</ul>
<li>7. Plugins</li>
<ul style="list-style:none;">
<li><a href="7.1-plugin-loadbalancer.php">7.1 Loadbalancer</a></li>
</ul>
</ul>
</body>
</html>
\ No newline at end of file
<?php
/**
* Copyright 2011 Bas de Nooijer. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this listof conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* The views and conclusions contained in the software and documentation are
* those of the authors and should not be interpreted as representing official
* policies, either expressed or implied, of the copyright holder.
*
* @copyright Copyright 2011 Bas de Nooijer <solarium@raspberry.nl>
* @license http://github.com/basdenooijer/solarium/raw/master/COPYING
* @link http://www.solarium-project.org/
*
* @package Solarium
*/
/**
* Loadbalancer plugin
*
* Using this plugin you can use software loadbalancing over multiple Solr instances.
* You can add any number of servers, each with their own weight. The weight influences
* the probability of a server being used for a query.
*
* By default all queries except updates are loadbalanced. This can be customized by setting blocked querytypes.
* Any querytype that may not be loadbalanced will be executed by Solarium with the default adapter settings.
* In a master-slave setup the default adapter should be connecting to the master server.
*
* You can also enable the failover mode. In this case a query will be retried in case of an error.
*
* @todo implement failover (including event trigger)
*
* @package Solarium
* @subpackage Plugin
*/
class Solarium_Plugin_Loadbalancer extends Solarium_Plugin_Abstract
{
/**
* Default options
*
* @var array
*/
protected $_options = array(
'failoverenabled' => false,
'failovermaxretries' => 1,
);
/**
* Registered servers
*
* @var array
*/
protected $_servers = array();
/**
* Query types that are blocked from loadbalancing
*
* @var array
*/
protected $_blockedQueryTypes = array(
Solarium_Client::QUERYTYPE_UPDATE
);
/**
* Key of the last used server
*
* The value can be null if no queries have been executed, or if the last executed query didn't use loadbalancing.
*
* @var null|string
*/
protected $_lastServerKey;
/**
* Server to use for next query (overrules randomizer)
*
* @var string
*/
protected $_nextServer;
/**
* Presets of the client adapter
*
* These settings are used to restore the adapter to it's original status for queries
* that cannot be loadbalanced (for instance update queries that need to go to the master)
*
* @var array
*/
protected $_adapterPresets;
/**
* Pool of servers to use for requests
*
* @var Solarium_Plugin_Loadbalancer_WeightedRandomChoice
*/
protected $_randomizer;
/**
* Query type
*
* @var string
*/
protected $_queryType;
/**
* Initialize options
*
* Several options need some extra checks or setup work, for these options
* the setters are called.
*
* @return void
*/
protected function _init()
{
foreach ($this->_options AS $name => $value) {
switch ($name) {
case 'server':
$this->setServers($value);
break;
case 'blockedquerytype':
$this->setBlockedQueryTypes($value);
break;
}
}
}
/**
* Set failover enabled option
*
* @param bool $value
* @return self Provides fluent interface
*/
public function setFailoverEnabled($value)
{
return $this->_setOption('failoverenabled', $value);
}
/**
* Get failoverenabled option
*
* @return boolean
*/
public function getFailoverEnabled()
{
return $this->getOption('failoverenabled');
}
/**
* Set failover max retries
*
* @param int $value
* @return self Provides fluent interface
*/
public function setFailoverMaxRetries($value)
{
return $this->_setOption('failovermaxretries', $value);
}
/**
* Get failovermaxretries option
*
* @return int
*/
public function getFailoverMaxRetries()
{
return $this->getOption('failovermaxretries');
}
/**
* Add a server to the loadbalacing 'pool'
*
* @param string $key
* @param array $options
* @param int $weight Must be a positive number
* @return self Provides fluent interface
*/
public function addServer($key, $options, $weight = 1) {
if (array_key_exists($key, $this->_servers)) {
throw new Solarium_Exception('A server for the loadbalancer plugin must have a unique key');
} else {
$this->_servers[$key] = array(
'options' => $options,
'weight' => $weight,
);
}
// reset the randomizer as soon as a new server is added
$this->_randomizer = null;
return $this;
}
/**
* Get servers in the loadbalancing pool
*
* @return array
*/
public function getServers()
{
return $this->_servers;
}
/**
* Get a server entry by key
*
* @param string $key
* @return array
*/
public function getServer($key)
{
if (!isset($this->_servers[$key])) {
throw new Solarium_Exception('Unknown server key');
}
return $this->_servers[$key];
}
/**
* Set servers, overwriting any existing servers
*
* @param array $servers Use server key as array key and 'options' and 'weight' as array entries
* @return self Provides fluent interface
*/
public function setServers($servers)
{
$this->clearServers();
$this->addServers($servers);
return $this;
}
/**
* Add multiple servers
*
* @param array $servers
* @return self Provides fluent interface
*/
public function addServers($servers)
{
foreach ($servers AS $key => $data) {
$this->addServer($key, $data['options'], $data['weight']);
}
return $this;
}
/**
* Clear all server entries
*
* @return self Provides fluent interface
*/
public function clearServers()
{
$this->_servers = array();
}
/**
* Remove a server by key
*
* @param string $key
* @return self Provides fluent interface
*/
public function removeServer($key)
{
if (isset($this->_servers[$key])) {
unset($this->_servers[$key]);
}
return $this;
}
/**
* Set a forced server (by key) for the next request
*
* As soon as one query has used the forced server this setting is reset.
*
* If the next query cannot be loadbalanced (for instance based on the querytype) this setting is ignored
* but will still be reset.
*
* @param string $key
* @return self Provides fluent interface
*/
public function forceServerForNextQuery($key)
{
if (!array_key_exists($key, $this->_servers)) {
throw new Solarium_Exception('Unknown server forced for next query');
}
$this->_nextServer = $key;
}
/**
* Get an array of blocked querytypes
*
* @return array
*/
public function getBlockedQueryTypes() {
return array_keys($this->_blockedQueryTypes);
}
/**
* Set querytypes to block from loadbalancing
*
* Overwrites any existing types
*
* @param array $types Use an array with the constants defined in Solarium_Client as values
* @return self Provides fluent interface
*/
public function setBlockedQueryTypes($types) {
$this->clearBlockedQueryTypes();
$this->addBlockedQueryTypes($types);
return $this;
}
/**
* Add a querytype to block from loadbalancing
*
* @param string $type Use one of the constants defined in Solarium_Client
* @return self Provides fluent interface
*/
public function addBlockedQueryType($type) {
if (!array_key_exists($type, $this->_blockedQueryTypes)) {
$this->_blockedQueryTypes[$type] = true;
}
return $this;
}
/**
* Add querytypes to block from loadbalancing
*
* Appended to any existing types
*
* @param array $types Use an array with the constants defined in Solarium_Client as values
* @return self Provides fluent interface
*/
public function addBlockedQueryTypes($types) {
foreach ($types AS $type) {
$this->addBlockedQueryType($type);
}
}
/**
* Remove a single querytype from the block list
*
* @param string $type
* @return void
*/
public function removeBlockedQueryType($type) {
if (array_key_exists($type, $this->_blockedQueryTypes)) {
unset($this->_blockedQueryTypes[$type]);
}
}
/**
* Clear all blocked querytypes
*
* @return self Provides fluent interface
*/
public function clearBlockedQueryTypes() {
$this->_blockedQueryTypes = array();
}
/**
* Event hook to capture querytype
*
* @param Solarium_Query $query
* @return void
*/
public function preCreateRequest($query)
{
$this->_queryType = $query->getType();
}
/**
* Event hook to adjust client settings just before query execution
*
* @param Solarium_Client_Request $request
* @return void
*/
public function preExecuteRequest($request)
{
$adapter = $this->_client->getAdapter();
// save adapter presets (once) to allow the settings to be restored later
if ($this->_adapterPresets == null) {
$this->_adapterPresets = array(
'host' => $adapter->getHost(),
'port' => $adapter->getPort(),
'path' => $adapter->getPath(),
'core' => $adapter->getCore(),
'timeout' => $adapter->getTimeout(),
);
}
// check querytype: is loadbalancing allowed?
if (!array_key_exists($this->_queryType, $this->_blockedQueryTypes)) {
// determine the server to use
if ($this->_nextServer !== null) {
$serverKey = $this->_nextServer;
} else {
$serverKey = $this->_getRandomizer()->getRandom();
}
$options = $this->_servers[$serverKey]['options'];
$this->_lastServerKey = $serverKey;
} else {
$options = $this->_adapterPresets;
$this->_lastServerKey = null;
}
// apply new settings to adapter
$adapter->setOptions($options);
// always reset forced server for next query
$this->_nextServer = null;
}
/**
* Get the key of the server that was used for the last query
*
* May return a null value if no query has been executed yet, or the last query could not be loadbalanced.
*
* @return null|string
*/
public function getLastServerKey()
{
return $this->_lastServerKey;
}
/**
* Get randomizer instance
*
* @return Solarium_Plugin_Loadbalancer_WeightedRandomChoice
*/
protected function _getRandomizer()
{
if ($this->_randomizer === null) {
$choices = array();
foreach($this->_servers AS $key => $settings) {
$choices[$key] = $settings['weight'];
}
$this->_randomizer = new Solarium_Plugin_Loadbalancer_WeightedRandomChoice($choices);
}
return $this->_randomizer;
}
}
\ No newline at end of file
<?php
/**
* Copyright 2011 Bas de Nooijer. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this listof conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* The views and conclusions contained in the software and documentation are
* those of the authors and should not be interpreted as representing official
* policies, either expressed or implied, of the copyright holder.
*
* @copyright Copyright 2011 Bas de Nooijer <solarium@raspberry.nl>
* @license http://github.com/basdenooijer/solarium/raw/master/COPYING
* @link http://www.solarium-project.org/
*
* @package Solarium
*/
/**
* Weighted random choice class
*
* For use in the loadbalancer plugin
*
* @package Solarium
* @subpackage Plugin
*/
class Solarium_Plugin_Loadbalancer_WeightedRandomChoice
{
/**
* Total weight of all choices
*
* @var int
*/
protected $_totalWeight = 0;
/**
* Choices total lookup array
*
* @var array
*/
protected $_lookup = array();
/**
* Values lookup array
*
* @var array
*/
protected $_values = array();
/**
* Constructor
*
* @param array $choices
*/
public function __construct($choices)
{
$i = 0;
foreach($choices AS $key => $weight) {
if ($weight <= 0) throw new Solarium_Exception('Weight must be greater than zero');
$this->_totalWeight += $weight;
$this->_lookup[$i] = $this->_totalWeight;
$this->_values[$i] = $key;
$i++;
}
}
/**
* Get a (weighted) random entry
*
* @return string
*/
public function getRandom()
{
return $this->_values[$this->_getKey()];
}
/**
* Get a (weighted) random entry key
*
* @return int
*/
protected function _getKey()
{
$random = mt_rand(1, $this->_totalWeight);
$high = count($this->_lookup)-1;
$low = 0;
while ( $low < $high ) {
$probe = (int)(($high + $low) / 2);
if ($this->_lookup[$probe] < $random) {
$low = $probe + 1;
} else if ($this->_lookup[$probe] > $random) {
$high = $probe - 1;
} else {
return $probe;
}
}
if ( $low != $high ) {
return $random;
} else {
if ($this->_lookup[$low] >= $random) {
return $low;
} else {
return $low+1;
}
}
}
}
\ No newline at end of file
<?php
/**
* Copyright 2011 Bas de Nooijer. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this listof conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* The views and conclusions contained in the software and documentation are
* those of the authors and should not be interpreted as representing official
* policies, either expressed or implied, of the copyright holder.
*/
class Solarium_Plugin_Loadbalancer_WeightedRandomChoiceTest extends PHPUnit_Framework_TestCase
{
public function testGetRandom()
{
$choices = array('key1' => 1, 'key2' => 2, 'key3' => 3);
$randomizer = new Solarium_Plugin_Loadbalancer_WeightedRandomChoice($choices);
$choice = $randomizer->getRandom();
$this->assertTrue(
array_key_exists($choice, $choices)
);
$counts = array('key1' => 0, 'key2' => 0, 'key3' => 0);
for ($i = 0; $i<1000; $i++) {
$choice = $randomizer->getRandom();
$counts[$choice]++;
}
$this->assertTrue($counts['key1'] < $counts['key2']);
$this->assertTrue($counts['key2'] < $counts['key3']);
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment