MediaWiki
REL1_24
|
00001 <?php 00029 class HashRing { 00031 protected $sourceMap = array(); 00033 protected $ring = array(); 00034 00036 protected $liveRing; 00038 protected $ejectionExpiries = array(); 00040 protected $ejectionNextExpiry = INF; 00041 00042 const RING_SIZE = 268435456; // 2^28 00043 00047 public function __construct( array $map ) { 00048 $map = array_filter( $map, function ( $w ) { 00049 return $w > 0; 00050 } ); 00051 if ( !count( $map ) ) { 00052 throw new UnexpectedValueException( "Ring is empty or all weights are zero." ); 00053 } 00054 $this->sourceMap = $map; 00055 // Sort the locations based on the hash of their names 00056 $hashes = array(); 00057 foreach ( $map as $location => $weight ) { 00058 $hashes[$location] = sha1( $location ); 00059 } 00060 uksort( $map, function ( $a, $b ) use ( $hashes ) { 00061 return strcmp( $hashes[$a], $hashes[$b] ); 00062 } ); 00063 // Fit the map to weight-proportionate one with a space of size RING_SIZE 00064 $sum = array_sum( $map ); 00065 $standardMap = array(); 00066 foreach ( $map as $location => $weight ) { 00067 $standardMap[$location] = (int)floor( $weight / $sum * self::RING_SIZE ); 00068 } 00069 // Build a ring of RING_SIZE spots, with each location at a spot in location hash order 00070 $index = 0; 00071 foreach ( $standardMap as $location => $weight ) { 00072 // Location covers half-closed interval [$index,$index + $weight) 00073 $this->ring[$location] = array( $index, $index + $weight ); 00074 $index += $weight; 00075 } 00076 // Make sure the last location covers what is left 00077 end( $this->ring ); 00078 $this->ring[key( $this->ring )][1] = self::RING_SIZE; 00079 } 00080 00087 public function getLocation( $item ) { 00088 $locations = $this->getLocations( $item, 1 ); 00089 00090 return $locations[0]; 00091 } 00092 00100 public function getLocations( $item, $limit ) { 00101 $locations = array(); 00102 $primaryLocation = null; 00103 $spot = hexdec( substr( sha1( $item ), 0, 7 ) ); // first 28 bits 00104 foreach ( $this->ring as $location => $range ) { 00105 if ( count( $locations ) >= $limit ) { 00106 break; 00107 } 00108 // The $primaryLocation is the location the item spot is in. 00109 // After that is reached, keep appending the next locations. 00110 if ( ( $range[0] <= $spot && $spot < $range[1] ) || $primaryLocation !== null ) { 00111 if ( $primaryLocation === null ) { 00112 $primaryLocation = $location; 00113 } 00114 $locations[] = $location; 00115 } 00116 } 00117 // If more locations are requested, wrap-around and keep adding them 00118 reset( $this->ring ); 00119 while ( count( $locations ) < $limit ) { 00120 list( $location, ) = each( $this->ring ); 00121 if ( $location === $primaryLocation ) { 00122 break; // don't go in circles 00123 } 00124 $locations[] = $location; 00125 } 00126 00127 return $locations; 00128 } 00129 00135 public function getLocationWeights() { 00136 return $this->sourceMap; 00137 } 00138 00145 public function newWithoutLocation( $location ) { 00146 $map = $this->sourceMap; 00147 unset( $map[$location] ); 00148 00149 return count( $map ) ? new self( $map ) : false; 00150 } 00151 00159 public function ejectFromLiveRing( $location, $ttl ) { 00160 if ( !isset( $this->sourceMap[$location] ) ) { 00161 throw new UnexpectedValueException( "No location '$location' in the ring." ); 00162 } 00163 $expiry = time() + $ttl; 00164 $this->liveRing = null; // stale 00165 $this->ejectionExpiries[$location] = $expiry; 00166 $this->ejectionNextExpiry = min( $expiry, $this->ejectionNextExpiry ); 00167 00168 return ( count( $this->ejectionExpiries ) < count( $this->sourceMap ) ); 00169 } 00170 00177 public function getLiveRing() { 00178 $now = time(); 00179 if ( $this->liveRing === null || $this->ejectionNextExpiry <= $now ) { 00180 $this->ejectionExpiries = array_filter( 00181 $this->ejectionExpiries, 00182 function( $expiry ) use ( $now ) { 00183 return ( $expiry > $now ); 00184 } 00185 ); 00186 if ( count( $this->ejectionExpiries ) ) { 00187 $map = array_diff_key( $this->sourceMap, $this->ejectionExpiries ); 00188 $this->liveRing = count( $map ) ? new self( $map ) : false; 00189 00190 $this->ejectionNextExpiry = min( $this->ejectionExpiries ); 00191 } else { // common case; avoid recalculating ring 00192 $this->liveRing = clone $this; 00193 $this->liveRing->ejectionExpiries = array(); 00194 $this->liveRing->ejectionNextExpiry = INF; 00195 $this->liveRing->liveRing = null; 00196 00197 $this->ejectionNextExpiry = INF; 00198 } 00199 } 00200 if ( !$this->liveRing ) { 00201 throw new UnexpectedValueException( "The live ring is currently empty." ); 00202 } 00203 00204 return $this->liveRing; 00205 } 00206 00214 public function getLiveLocation( $item ) { 00215 return $this->getLiveRing()->getLocation( $item ); 00216 } 00217 00226 public function getLiveLocations( $item ) { 00227 return $this->getLiveRing()->getLocations( $item ); 00228 } 00229 00236 public function getLiveLocationWeights() { 00237 return $this->getLiveRing()->getLocationWeights(); 00238 } 00239 }