MediaWiki  REL1_24
HashRing.php
Go to the documentation of this file.
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 }