270 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			270 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| 
 | |
| interface Flexihash_Hasher
 | |
| {
 | |
| 	public function hash($string);
 | |
| }
 | |
| 
 | |
| class Flexihash_Crc32Hasher
 | |
| 	implements Flexihash_Hasher
 | |
| {
 | |
| 
 | |
| 	public function hash($string)
 | |
| 	{
 | |
| 		return crc32($string);
 | |
| 	}
 | |
| 
 | |
| }
 | |
| 
 | |
| class Flexihash_Exception extends Exception
 | |
| {
 | |
| }
 | |
| 
 | |
| 
 | |
| 
 | |
| /**
 | |
|  * A simple consistent hashing implementation with pluggable hash algorithms.
 | |
|  *
 | |
|  * @author Paul Annesley
 | |
|  * @package Flexihash
 | |
|  * @licence http://www.opensource.org/licenses/mit-license.php
 | |
|  */
 | |
| class Flexihash
 | |
| {
 | |
| 
 | |
| 	/**
 | |
| 	 * The number of positions to hash each target to.
 | |
| 	 *
 | |
| 	 * @var int
 | |
| 	 */
 | |
| 	private $_replicas = 64;
 | |
| 
 | |
| 	/**
 | |
| 	 * The hash algorithm, encapsulated in a Flexihash_Hasher implementation.
 | |
| 	 * @var object Flexihash_Hasher
 | |
| 	 */
 | |
| 	private $_hasher;
 | |
| 
 | |
| 	/**
 | |
| 	 * Internal counter for current number of targets.
 | |
| 	 * @var int
 | |
| 	 */
 | |
| 	private $_targetCount = 0;
 | |
| 
 | |
| 	/**
 | |
| 	 * Internal map of positions (hash outputs) to targets
 | |
| 	 * @var array { position => target, ... }
 | |
| 	 */
 | |
| 	private $_positionToTarget = array();
 | |
| 
 | |
| 	/**
 | |
| 	 * Internal map of targets to lists of positions that target is hashed to.
 | |
| 	 * @var array { target => [ position, position, ... ], ... }
 | |
| 	 */
 | |
| 	private $_targetToPositions = array();
 | |
| 
 | |
| 	/**
 | |
| 	 * Whether the internal map of positions to targets is already sorted.
 | |
| 	 * @var boolean
 | |
| 	 */
 | |
| 	private $_positionToTargetSorted = false;
 | |
| 
 | |
| 	/**
 | |
| 	 * Constructor
 | |
| 	 * @param object $hasher Flexihash_Hasher
 | |
| 	 * @param int $replicas Amount of positions to hash each target to.
 | |
| 	 */
 | |
| 	public function __construct(Flexihash_Hasher $hasher = null, $replicas = null)
 | |
| 	{
 | |
| 		$this->_hasher = $hasher ? $hasher : new Flexihash_Crc32Hasher();
 | |
| 		if (!empty($replicas)) $this->_replicas = $replicas;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Add a target.
 | |
| 	 * @param string $target
 | |
|          * @param float $weight
 | |
| 	 * @chainable
 | |
| 	 */
 | |
| 	public function addTarget($target, $weight=1)
 | |
| 	{
 | |
| 		if (isset($this->_targetToPositions[$target]))
 | |
| 		{
 | |
| 			throw new Flexihash_Exception("Target '$target' already exists.");
 | |
| 		}
 | |
| 
 | |
| 		$this->_targetToPositions[$target] = array();
 | |
| 
 | |
| 		// hash the target into multiple positions
 | |
| 		for ($i = 0; $i < round($this->_replicas*$weight); $i++)
 | |
| 		{
 | |
| 			$position = $this->_hasher->hash($target . $i);
 | |
| 			$this->_positionToTarget[$position] = $target; // lookup
 | |
| 			$this->_targetToPositions[$target] []= $position; // target removal
 | |
| 		}
 | |
| 
 | |
| 		$this->_positionToTargetSorted = false;
 | |
| 		$this->_targetCount++;
 | |
| 
 | |
| 		return $this;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Add a list of targets.
 | |
| 	 * @param array $targets
 | |
|          * @param float $weight
 | |
| 	 * @chainable
 | |
| 	 */
 | |
| 	public function addTargets($targets, $weight=1)
 | |
| 	{
 | |
| 		foreach ($targets as $target)
 | |
| 		{
 | |
| 			$this->addTarget($target,$weight);
 | |
| 		}
 | |
| 
 | |
| 		return $this;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Remove a target.
 | |
| 	 * @param string $target
 | |
| 	 * @chainable
 | |
| 	 */
 | |
| 	public function removeTarget($target)
 | |
| 	{
 | |
| 		if (!isset($this->_targetToPositions[$target]))
 | |
| 		{
 | |
| 			throw new Flexihash_Exception("Target '$target' does not exist.");
 | |
| 		}
 | |
| 
 | |
| 		foreach ($this->_targetToPositions[$target] as $position)
 | |
| 		{
 | |
| 			unset($this->_positionToTarget[$position]);
 | |
| 		}
 | |
| 
 | |
| 		unset($this->_targetToPositions[$target]);
 | |
| 
 | |
| 		$this->_targetCount--;
 | |
| 
 | |
| 		return $this;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * A list of all potential targets
 | |
| 	 * @return array
 | |
| 	 */
 | |
| 	public function getAllTargets()
 | |
| 	{
 | |
| 		return array_keys($this->_targetToPositions);
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Looks up the target for the given resource.
 | |
| 	 * @param string $resource
 | |
| 	 * @return string
 | |
| 	 */
 | |
| 	public function lookup($resource)
 | |
| 	{
 | |
| 		$targets = $this->lookupList($resource, 1);
 | |
| 		if (empty($targets)) throw new Flexihash_Exception('No targets exist');
 | |
| 		return $targets[0];
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Get a list of targets for the resource, in order of precedence.
 | |
| 	 * Up to $requestedCount targets are returned, less if there are fewer in total.
 | |
| 	 *
 | |
| 	 * @param string $resource
 | |
| 	 * @param int $requestedCount The length of the list to return
 | |
| 	 * @return array List of targets
 | |
| 	 */
 | |
| 	public function lookupList($resource, $requestedCount)
 | |
| 	{
 | |
| 		if (!$requestedCount)
 | |
| 			throw new Flexihash_Exception('Invalid count requested');
 | |
| 
 | |
| 		// handle no targets
 | |
| 		if (empty($this->_positionToTarget))
 | |
| 			return array();
 | |
| 
 | |
| 		// optimize single target
 | |
| 		if ($this->_targetCount == 1)
 | |
| 			return array_unique(array_values($this->_positionToTarget));
 | |
| 
 | |
| 		// hash resource to a position
 | |
| 		$resourcePosition = $this->_hasher->hash($resource);
 | |
| 
 | |
| 		$results = array();
 | |
| 		$collect = false;
 | |
| 
 | |
| 		$this->_sortPositionTargets();
 | |
| 
 | |
| 		// search values above the resourcePosition
 | |
| 		foreach ($this->_positionToTarget as $key => $value)
 | |
| 		{
 | |
| 			// start collecting targets after passing resource position
 | |
| 			if (!$collect && $key > $resourcePosition)
 | |
| 			{
 | |
| 				$collect = true;
 | |
| 			}
 | |
| 
 | |
| 			// only collect the first instance of any target
 | |
| 			if ($collect && !in_array($value, $results))
 | |
| 			{
 | |
| 				$results []= $value;
 | |
| 			}
 | |
| 
 | |
| 			// return when enough results, or list exhausted
 | |
| 			if (count($results) == $requestedCount || count($results) == $this->_targetCount)
 | |
| 			{
 | |
| 				return $results;
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// loop to start - search values below the resourcePosition
 | |
| 		foreach ($this->_positionToTarget as $key => $value)
 | |
| 		{
 | |
| 			if (!in_array($value, $results))
 | |
| 			{
 | |
| 				$results []= $value;
 | |
| 			}
 | |
| 
 | |
| 			// return when enough results, or list exhausted
 | |
| 			if (count($results) == $requestedCount || count($results) == $this->_targetCount)
 | |
| 			{
 | |
| 				return $results;
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// return results after iterating through both "parts"
 | |
| 		return $results;
 | |
| 	}
 | |
| 
 | |
| 	public function __toString()
 | |
| 	{
 | |
| 		return sprintf(
 | |
| 			'%s{targets:[%s]}',
 | |
| 			get_class($this),
 | |
| 			implode(',', $this->getAllTargets())
 | |
| 		);
 | |
| 	}
 | |
| 
 | |
| 	// ----------------------------------------
 | |
| 	// private methods
 | |
| 
 | |
| 	/**
 | |
| 	 * Sorts the internal mapping (positions to targets) by position
 | |
| 	 */
 | |
| 	private function _sortPositionTargets()
 | |
| 	{
 | |
| 		// sort by key (position) if not already
 | |
| 		if (!$this->_positionToTargetSorted)
 | |
| 		{
 | |
| 			ksort($this->_positionToTarget, SORT_REGULAR);
 | |
| 			$this->_positionToTargetSorted = true;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| }
 | |
| 
 |