53 const RING_SIZE = 4294967296.0;
55 const HASHES_PER_LOCATION = 40;
57 const SECTORS_PER_HASH = 4;
85 if ( !in_array(
$algo, hash_algos(),
true ) ) {
86 throw new RuntimeException( __METHOD__ .
": unsupported '$algo' hash algorithm." );
91 throw new UnexpectedValueException(
"No locations with non-zero weight." );
92 } elseif ( min( $map ) < 0 ) {
93 throw new InvalidArgumentException(
"Location weight cannot be negative." );
98 $this->ejectExpiryByLocation = $ejections;
99 $this->baseRing = $this->
buildLocationRing( $this->weightByLocation, $this->algo );
122 public function getLocations( $item, $limit, $from = self::RING_ALL ) {
123 if ( $from === self::RING_ALL ) {
125 } elseif ( $from === self::RING_LIVE ) {
128 throw new InvalidArgumentException(
"Invalid ring source specified." );
136 $currentIndex = $itemNodeIndex;
137 while (
count( $locations ) < $limit ) {
139 if ( !in_array( $nodeLocation, $locations,
true ) ) {
141 $locations[] = $nodeLocation;
144 if ( $currentIndex === $itemNodeIndex ) {
158 $count =
count( $ring );
159 if ( $count === 0 ) {
165 $midPos = intval( ( $lowPos + $highPos ) / 2 );
166 if ( $midPos === $count ) {
170 $midMinusOneVal = $midPos === 0 ? 0 : $ring[$midPos - 1][
self::KEY_POS];
172 if ( $position <= $midVal && $position > $midMinusOneVal ) {
176 if ( $midVal < $position ) {
177 $lowPos = $midPos + 1;
179 $highPos = $midPos - 1;
182 if ( $lowPos > $highPos ) {
206 if ( !isset( $this->weightByLocation[$location] ) ) {
207 throw new UnexpectedValueException(
"No location '$location' in the ring." );
211 $this->ejectExpiryByLocation[$location] = $expiry;
213 $this->liveRing =
null;
215 return (
count( $this->ejectExpiryByLocation ) <
count( $this->weightByLocation ) );
226 return $this->
getLocations( $item, 1, self::RING_LIVE )[0];
238 return $this->
getLocations( $item, $limit, self::RING_LIVE );
250 return array_diff_key(
251 $this->weightByLocation,
253 $this->ejectExpiryByLocation,
254 function ( $expiry )
use ( $now ) {
255 return ( $expiry > $now );
274 $ratio = $weight / $totalWeight;
277 $nodesQuartets = intval( $ratio * self::HASHES_PER_LOCATION * $locationCount );
278 for ( $qi = 0; $qi < $nodesQuartets; ++$qi ) {
283 foreach ( $positions
as $gi => $position ) {
284 $node = ( $qi * self::SECTORS_PER_HASH + $gi ) .
"@$location";
285 $posKey = (
string)$position;
286 if ( isset( $claimed[$posKey] ) ) {
288 if ( $claimed[$posKey][
'node'] > $node ) {
291 unset( $ring[$claimed[$posKey][
'index']] );
295 self::KEY_POS => $position,
296 self::KEY_LOCATION => $location
298 $claimed[$posKey] = [
'node' => $node,
'index' =>
count( $ring ) - 1 ];
303 usort( $ring,
function ( $a, $b ) {
304 if ( $a[self::KEY_POS] === $b[self::KEY_POS] ) {
305 throw new UnexpectedValueException(
'Duplicate node positions.' );
308 return ( $a[self::KEY_POS] < $b[self::KEY_POS] ? -1 : 1 );
321 $octets = substr( hash( $this->algo, (
string)$item,
true ), 0, 4 );
322 if ( strlen( $octets ) != 4 ) {
323 throw new UnexpectedValueException( __METHOD__ .
": {$this->algo} is < 32 bits." );
326 return (
float)sprintf(
'%u', unpack(
'V', $octets )[1] );
334 $octets = substr( hash( $this->algo, (
string)$nodeGroupName,
true ), 0, 16 );
335 if ( strlen( $octets ) != 16 ) {
336 throw new UnexpectedValueException( __METHOD__ .
": {$this->algo} is < 128 bits." );
340 foreach ( unpack(
'V4', $octets )
as $signed ) {
341 $positions[] = (float)sprintf(
'%u', $signed );
353 if ( !isset( $ring[$i] ) ) {
354 throw new UnexpectedValueException( __METHOD__ .
": reference index is invalid." );
359 return ( $next <
count( $ring ) ) ? $next : 0;
369 if ( !$this->ejectExpiryByLocation ) {
375 if ( $this->liveRing ===
null || min( $this->ejectExpiryByLocation ) <= $now ) {
377 $this->ejectExpiryByLocation = array_filter(
378 $this->ejectExpiryByLocation,
379 function ( $expiry )
use ( $now ) {
380 return ( $expiry > $now );
384 if (
count( $this->ejectExpiryByLocation ) ) {
387 foreach ( $this->baseRing
as $i => $nodeInfo ) {
389 if ( !isset( $this->ejectExpiryByLocation[$location] ) ) {
400 if ( !$this->liveRing ) {
401 throw new UnexpectedValueException(
"The live ring is currently empty." );
416 'algorithm' => $this->algo,
417 'locations' => $this->weightByLocation,
418 'ejections' => $this->ejectExpiryByLocation
424 if ( is_array(
$data ) ) {
427 throw new UnexpectedValueException( __METHOD__ .
": unable to decode JSON." );