Repository: pda/flexihash
Branch: master
Commit: 8403c28a5453
Files: 19
Total size: 33.5 KB
Directory structure:
gitextract_0n0hhiec/
├── .coveralls.yml
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── LICENCE
├── README.md
├── ROADMAP.md
├── composer.json
├── phpcs.xml
├── phpunit.xml.dist
├── src/
│ ├── Exception.php
│ ├── Flexihash.php
│ └── Hasher/
│ ├── Crc32Hasher.php
│ ├── HasherInterface.php
│ └── Md5Hasher.php
└── tests/
├── BenchmarkTest.php
├── FlexihashTest.php
└── Hasher/
├── HasherTest.php
└── MockHasher.php
================================================
FILE CONTENTS
================================================
================================================
FILE: .coveralls.yml
================================================
coverage_clover: clover.xml
json_path: coveralls-upload.json
================================================
FILE: .gitignore
================================================
/vendor/
================================================
FILE: .travis.yml
================================================
language: php
env:
global:
- COVERALLS=0
- PHPCS=0
matrix:
include:
- php: 7.2
- php: 7.3
- php: 7.4
env: COVERALLS=1 PHPCS=1
- php: hhvm
- php: nightly
allow_failures:
- php: hhvm
- php: nightly
fast_finish: true
before_script:
- composer install --no-interaction --prefer-source --dev
script:
- sh -c "if [ '$COVERALLS' = '1' ]; then ./vendor/bin/phpunit --coverage-clover clover.xml ; else ./vendor/bin/phpunit ; fi"
- sh -c "if [ '$PHPCS' = '1' ]; then vendor/bin/phpcs -p --extensions=php --standard=./phpcs.xml ./src ./tests ; fi"
after_script:
- sh -c "if [ '$COVERALLS' = '1' ]; then vendor/bin/coveralls ; fi"
notifications:
on_success: never
on_failure: always
================================================
FILE: CHANGELOG.md
================================================
# Change Log
All notable changes to this project will be documented in this file
which adheres to the guidelines at http://keepachangelog.com/.
This project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased]
## [2.0.2] - 2016-04-22
### Changed
- Pinned symfony component version to pass tests on 5.4.x.
- Updated coveralls config for new version.
- Tweaked README.md to recommend install version 2.
- Sorted phpcs errors.
## [2.0.1] - 2016-04-22
### Changed
- Make MD5 hasher return an integer to prevent incorrect remapping
due to PHP treating numeric string array keys as integers.
## [2.0.0] - 2015-10-08
### Added
- This CHANGELOG.md file.
- A ROADMAP.md file.
- PSR-4 autoloading.
- Introduce namespacing.
- Full PSR-2 support.
### Changed
- Reorganisation of files.
- Updated readme to reflect composer installation recommendation.
### Removed
- PHP<5.4 support
## [1.0.0] - 2015-10-16
### Added
- Setup automatic testing with Travis.
- Monitor code coverage with Coveralls.
- Get as close to PSR-2 as possible without changing class names.
### Changed
- Migrate tests to PHPUnit.
### Removed
- Legacy autoloader.
## [0.1.0] - 2012-04-04
Posterity release
[Unreleased]: https://github.com/pda/flexihash/compare/v2.0.2...master
[2.0.2]: https://github.com/pda/flexihash/compare/v2.0.1...v2.0.2
[2.0.1]: https://github.com/pda/flexihash/compare/v2.0.0...v2.0.1
[2.0.0]: https://github.com/pda/flexihash/compare/v1.0.0...v2.0.0
[1.0.0]: https://github.com/pda/flexihash/compare/v0.1.0...v1.0.0
[0.1.0]: https://github.com/pda/flexihash/tree/v0.1.0
================================================
FILE: LICENCE
================================================
The MIT License
Copyright (c) 2008 Paul Annesley
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: README.md
================================================
# Flexihash
[](https://travis-ci.org/pda/flexihash) [](https://coveralls.io/github/pda/flexihash?branch=master)
Flexihash is a small PHP library which implements [consistent hashing](http://en.wikipedia.org/wiki/Consistent_hashing), which is most useful in distributed caching. It requires PHP5 and uses [PHPUnit](http://simpletest.org/) for unit testing.
## Installation
[Composer](https://getcomposer.org/) is the recommended installation technique. You can find flexihash on [Packagist](https://packagist.org/packages/flexihash/flexihash) so installation is as easy as
```
composer require flexihash/flexihash
```
or in your `composer.json`
```json
{
"require": {
"flexihash/flexihash": "^3.0.0"
}
}
```
## Usage
```php
$hash = new Flexihash();
// bulk add
$hash->addTargets(['cache-1', 'cache-2', 'cache-3']);
// simple lookup
$hash->lookup('object-a'); // "cache-1"
$hash->lookup('object-b'); // "cache-2"
// add and remove
$hash
->addTarget('cache-4')
->removeTarget('cache-1');
// lookup with next-best fallback (for redundant writes)
$hash->lookupList('object', 2); // ["cache-2", "cache-4"]
// remove cache-2, expect object to hash to cache-4
$hash->removeTarget('cache-2');
$hash->lookup('object'); // "cache-4"
```
## Tests
### Unit Test
```
% vendor/bin/phpunit
```
### Benchmark Test
```
% vendor/bin/phpunit tests/BenchmarkTest.php
```
## Further Reading
* http://www.spiteful.com/2008/03/17/programmers-toolbox-part-3-consistent-hashing/
* http://weblogs.java.net/blog/tomwhite/archive/2007/11/consistent_hash.html
================================================
FILE: ROADMAP.md
================================================
#Roadmap
## v1.0.0
This maintains the historical API but allows for composer autoloading.
- [x] Composer support.
- [x] PSR2 bar class names.
- [x] Migrate tests to PHPUnit.
## v2.0.0
The historical API will be broken by classname changes.
- [x] Introduce namespacing.
- [x] PSR4 autoloading.
- [x] Automated testing.
- [x] PHP 5.4 minimum.
## v3.0.0
- [x] PHP 7.2 minimum.
- [x] PHPUnit 8.
- [x] Enable strict typing mode for all PHP files.
## v4.0.0
- [ ] PHP 7.3 minimum.
- [ ] PHPUnit 9.
================================================
FILE: composer.json
================================================
{
"name": "flexihash/flexihash",
"type": "library",
"description": "Flexihash is a small PHP library which implements consistent hashing",
"homepage": "https://github.com/pda/flexihash",
"license": "MIT",
"authors": [
{
"name": "Paul Annesley",
"email": "paul@annesley.cc",
"homepage": "http://paul.annesley.cc"
},
{
"name": "Dom Morgan",
"email": "dom@d3r.com",
"homepage": "https://d3r.com"
}
],
"require": {
"php": ">=7.2.0"
},
"require-dev": {
"phpunit/phpunit": "^8",
"squizlabs/php_codesniffer": "3.*",
"php-coveralls/php-coveralls": "^2.2",
"symfony/config": "^5.1.3",
"symfony/console": "^5.1.3",
"symfony/filesystem": "^5.1.3",
"symfony/stopwatch": "^5.1.3",
"symfony/yaml": "^5.1.3"
},
"autoload": {
"psr-4": {
"Flexihash\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Flexihash\\Tests\\": "tests/"
}
}
}
================================================
FILE: phpcs.xml
================================================
Flexihash Coding Standard
================================================
FILE: phpunit.xml.dist
================================================
tests
tests/BenchmarkTest.php
================================================
FILE: src/Exception.php
================================================
target, ... }
*/
private $positionToTarget = [];
/**
* Internal map of targets to lists of positions that target is hashed to.
* @var array { target => [ position, position, ... ], ... }
*/
private $targetToPositions = [];
/**
* Whether the internal map of positions to targets is already sorted.
* @var bool
*/
private $positionToTargetSorted = false;
/**
* Sorted positions.
*
* @var array
*/
private $sortedPositions = [];
/**
* Internal counter for current number of positions.
*
* @var integer
*/
private $positionCount = 0;
/**
* Constructor.
* @param \Flexihash\Hasher\HasherInterface $hasher
* @param int $replicas Amount of positions to hash each target to.
*/
public function __construct(HasherInterface $hasher = null, $replicas = null)
{
$this->hasher = $hasher ? $hasher : new 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 Exception("Target '$target' already exists.");
}
$this->targetToPositions[$target] = [];
// 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
* @return self fluent
*/
public function addTargets($targets, $weight = 1)
{
foreach ($targets as $target) {
$this->addTarget($target, $weight);
}
return $this;
}
/**
* Remove a target.
*
* @param string $target
* @return self fluent
* @throws \Flexihash\Exception when target does not exist
*/
public function removeTarget($target)
{
if (!isset($this->targetToPositions[$target])) {
throw new Exception("Target '$target' does not exist.");
}
foreach ($this->targetToPositions[$target] as $position) {
unset($this->positionToTarget[$position]);
}
unset($this->targetToPositions[$target]);
$this->positionToTargetSorted = false;
--$this->targetCount;
return $this;
}
/**
* A list of all potential targets.
* @return array
*/
public function getAllTargets(): array
{
return array_keys($this->targetToPositions);
}
/**
* Looks up the target for the given resource.
* @param string $resource
* @return string
* @throws \Flexihash\Exception when no targets defined
*/
public function lookup($resource): string
{
$targets = $this->lookupList($resource, 1);
if (empty($targets)) {
throw new 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
* @throws \Flexihash\Exception when count is invalid
*/
public function lookupList($resource, $requestedCount): array
{
if (!$requestedCount) {
throw new Exception('Invalid count requested');
}
// handle no targets
if (empty($this->positionToTarget)) {
return [];
}
// 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 = [];
$this->sortPositionTargets();
$positions = $this->sortedPositions;
$low = 0;
$high = $this->positionCount - 1;
$notfound = false;
// binary search of the first position greater than resource position
while ($high >= $low || $notfound = true) {
$probe = (int) floor(($high + $low) / 2);
if ($notfound === false && $positions[$probe] <= $resourcePosition) {
$low = $probe + 1;
} elseif ($probe === 0 || $resourcePosition > $positions[$probe - 1] || $notfound === true) {
if ($notfound) {
// if not found is true, it means binary search failed to find any position greater
// than ressource position, in this case, the last position is the bigest lower
// position and first position is the next one after cycle
$probe = 0;
}
$results[] = $this->positionToTarget[$positions[$probe]];
if ($requestedCount > 1) {
for ($i = $requestedCount - 1; $i > 0; --$i) {
if (++$probe > $this->positionCount - 1) {
$probe = 0; // cycle
}
$results[] = $this->positionToTarget[$positions[$probe]];
}
}
break;
} else {
$high = $probe - 1;
}
}
return array_unique($results);
}
public function __toString(): string
{
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;
$this->sortedPositions = array_keys($this->positionToTarget);
$this->positionCount = count($this->sortedPositions);
}
}
}
================================================
FILE: src/Hasher/Crc32Hasher.php
================================================
*/
public function hash($string)
{
return hexdec(substr(md5($string), 0, 8));
}
}
================================================
FILE: tests/BenchmarkTest.php
================================================
lookups) as $i) {
$results1[$i] = $this->basicHash("t$i", 10);
}
$results2 = [];
foreach (range(1, $this->lookups) as $i) {
$results2[$i] = $this->basicHash("t$i", 11);
}
$differences = 0;
foreach (range(1, $this->lookups) as $i) {
if ($results1[$i] !== $results2[$i]) {
++$differences;
}
}
$percent = round($differences / $this->lookups * 100);
$this->dump("NonConsistentHash: {$percent}% of lookups changed ".
"after adding a target to the existing {$this->targets}");
}
public function testRemoveTargetWithNonConsistentHash(): void
{
$results1 = [];
foreach (range(1, $this->lookups) as $i) {
$results1[$i] = $this->basicHash("t$i", 10);
}
$results2 = [];
foreach (range(1, $this->lookups) as $i) {
$results2[$i] = $this->basicHash("t$i", 9);
}
$differences = 0;
foreach (range(1, $this->lookups) as $i) {
if ($results1[$i] !== $results2[$i]) {
++$differences;
}
}
$percent = round($differences / $this->lookups * 100);
$this->dump("NonConsistentHash: {$percent}% of lookups changed ".
"after removing 1 of {$this->targets} targets");
}
public function testHopeAddingTargetDoesNotChangeMuchWithCrc32Hasher(): void
{
$hashSpace = new Flexihash(
new Crc32Hasher()
);
foreach (range(1, $this->targets) as $i) {
$hashSpace->addTarget("target$i");
}
$results1 = [];
foreach (range(1, $this->lookups) as $i) {
$results1[$i] = $hashSpace->lookup("t$i");
}
$hashSpace->addTarget('target-new');
$results2 = [];
foreach (range(1, $this->lookups) as $i) {
$results2[$i] = $hashSpace->lookup("t$i");
}
$differences = 0;
foreach (range(1, $this->lookups) as $i) {
if ($results1[$i] !== $results2[$i]) {
++$differences;
}
}
$percent = round($differences / $this->lookups * 100);
$this->dump("ConsistentHash: {$percent}% of lookups changed ".
"after adding a target to the existing {$this->targets}");
}
public function testHopeRemovingTargetDoesNotChangeMuchWithCrc32Hasher(): void
{
$hashSpace = new Flexihash(
new Crc32Hasher()
);
foreach (range(1, $this->targets) as $i) {
$hashSpace->addTarget("target$i");
}
$results1 = [];
foreach (range(1, $this->lookups) as $i) {
$results1[$i] = $hashSpace->lookup("t$i");
}
$hashSpace->removeTarget('target1');
$results2 = [];
foreach (range(1, $this->lookups) as $i) {
$results2[$i] = $hashSpace->lookup("t$i");
}
$differences = 0;
foreach (range(1, $this->lookups) as $i) {
if ($results1[$i] !== $results2[$i]) {
++$differences;
}
}
$percent = round($differences / $this->lookups * 100);
$this->dump("ConsistentHash: {$percent}% of lookups changed ".
"after removing 1 of {$this->targets} targets");
}
public function testHashDistributionWithCrc32Hasher(): void
{
$hashSpace = new Flexihash(
new Crc32Hasher()
);
foreach (range(1, $this->targets) as $i) {
$hashSpace->addTarget("target$i");
}
$results = [];
foreach (range(1, $this->lookups) as $i) {
$results[$i] = $hashSpace->lookup("t$i");
}
$distribution = [];
foreach ($hashSpace->getAllTargets() as $target) {
$distribution[$target] = count(array_keys($results, $target));
}
$this->dump(sprintf(
'Distribution of %d lookups per target (min/max/median/avg): %d/%d/%d/%d',
$this->lookups / $this->targets,
min($distribution),
max($distribution),
round($this->median($distribution)),
round(array_sum($distribution) / count($distribution))
));
}
public function testHasherSpeed(): void
{
$hashCount = 100000;
$md5Hasher = new Md5Hasher();
$crc32Hasher = new Crc32Hasher();
$start = microtime(true);
for ($i = 0; $i < $hashCount; ++$i) {
$md5Hasher->hash("test$i");
}
$timeMd5 = microtime(true) - $start;
$start = microtime(true);
for ($i = 0; $i < $hashCount; ++$i) {
$crc32Hasher->hash("test$i");
}
$timeCrc32 = microtime(true) - $start;
$this->dump(sprintf(
'Hashers timed over %d hashes (MD5 / CRC32): %f / %f',
$hashCount,
$timeMd5,
$timeCrc32
));
}
// ----------------------------------------
private function basicHash($value, $targets):int
{
return abs(crc32($value) % $targets);
}
/**
* @param array $array list of numeric values
* @return numeric
*/
private function median($values):int
{
$values = array_values($values);
sort($values);
$count = count($values);
$middleFloor = floor($count / 2);
if ($count % 2 == 1) {
return $values[$middleFloor];
} else {
return ($values[$middleFloor] + $values[$middleFloor + 1]) / 2;
}
}
}
================================================
FILE: tests/FlexihashTest.php
================================================
assertEquals($hashSpace->getAllTargets(), []);
}
public function testAddTargetThrowsExceptionOnDuplicateTarget(): void
{
$hashSpace = new Flexihash();
$hashSpace->addTarget('t-a');
$this->expectException('Flexihash\Exception');
$hashSpace->addTarget('t-a');
}
public function testAddTargetAndGetAllTargets(): void
{
$hashSpace = new Flexihash();
$hashSpace
->addTarget('t-a')
->addTarget('t-b')
->addTarget('t-c')
;
$this->assertEquals($hashSpace->getAllTargets(), ['t-a', 't-b', 't-c']);
}
public function testAddTargetsAndGetAllTargets(): void
{
$targets = ['t-a', 't-b', 't-c'];
$hashSpace = new Flexihash();
$hashSpace->addTargets($targets);
$this->assertEquals($hashSpace->getAllTargets(), $targets);
}
public function testRemoveTarget(): void
{
$hashSpace = new Flexihash();
$hashSpace
->addTarget('t-a')
->addTarget('t-b')
->addTarget('t-c')
->removeTarget('t-b')
;
$this->assertEquals($hashSpace->getAllTargets(), ['t-a', 't-c']);
}
public function testRemoveTargetFailsOnMissingTarget(): void
{
$hashSpace = new Flexihash();
$this->expectException('Flexihash\Exception');
$hashSpace->removeTarget('not-there');
}
public function testHashSpaceRepeatableLookups(): void
{
$hashSpace = new Flexihash();
foreach (range(1, 10) as $i) {
$hashSpace->addTarget("target$i");
}
$this->assertEquals($hashSpace->lookup('t1'), $hashSpace->lookup('t1'));
$this->assertEquals($hashSpace->lookup('t2'), $hashSpace->lookup('t2'));
}
public function testHashSpaceLookupListEmpty(): void
{
$hashSpace = new Flexihash();
$this->assertEmpty($hashSpace->lookupList('t1', 2));
}
public function testHashSpaceLookupListNoTargets(): void
{
$this->expectException('Flexihash\Exception');
$this->expectExceptionMessage('No targets exist');
$hashSpace = new Flexihash();
$hashSpace->lookup('t1');
}
public function testHashSpaceLookupListNo(): void
{
$this->expectException('Flexihash\Exception');
$this->expectExceptionMessage('Invalid count requested');
$hashSpace = new Flexihash();
$hashSpace->lookupList('t1', 0);
}
public function testHashSpaceLookupsAreValidTargets(): void
{
$targets = [];
foreach (range(1, 10) as $i) {
$targets [] = "target$i";
}
$hashSpace = new Flexihash();
$hashSpace->addTargets($targets);
foreach (range(1, 10) as $i) {
$this->assertTrue(
in_array($hashSpace->lookup("r$i"), $targets),
'target must be in list of targets'
);
}
}
public function testHashSpaceConsistentLookupsAfterAddingAndRemoving(): void
{
$hashSpace = new Flexihash();
foreach (range(1, 10) as $i) {
$hashSpace->addTarget("target$i");
}
$results1 = [];
foreach (range(1, 100) as $i) {
$results1 [] = $hashSpace->lookup("t$i");
}
$hashSpace
->addTarget('new-target')
->removeTarget('new-target')
->addTarget('new-target')
->removeTarget('new-target')
;
$results2 = [];
foreach (range(1, 100) as $i) {
$results2 [] = $hashSpace->lookup("t$i");
}
// This is probably optimistic, as adding/removing a target may
// clobber existing targets and is not expected to restore them.
$this->assertEquals($results1, $results2);
}
public function testHashSpaceConsistentLookupsWithNewInstance(): void
{
$hashSpace1 = new Flexihash();
foreach (range(1, 10) as $i) {
$hashSpace1->addTarget("target$i");
}
$results1 = [];
foreach (range(1, 100) as $i) {
$results1 [] = $hashSpace1->lookup("t$i");
}
$hashSpace2 = new Flexihash();
foreach (range(1, 10) as $i) {
$hashSpace2->addTarget("target$i");
}
$results2 = [];
foreach (range(1, 100) as $i) {
$results2 [] = $hashSpace2->lookup("t$i");
}
$this->assertEquals($results1, $results2);
}
public function testGetMultipleTargets(): void
{
$hashSpace = new Flexihash();
foreach (range(1, 10) as $i) {
$hashSpace->addTarget("target$i");
}
$targets = $hashSpace->lookupList('resource', 2);
$this->assertIsArray($targets);
$this->assertEquals(count($targets), 2);
$this->assertNotEquals($targets[0], $targets[1]);
}
public function testGetMultipleTargetsWithOnlyOneTarget(): void
{
$hashSpace = new Flexihash();
$hashSpace->addTarget('single-target');
$targets = $hashSpace->lookupList('resource', 2);
$this->assertIsArray($targets);
$this->assertEquals(count($targets), 1);
$this->assertEquals($targets[0], 'single-target');
}
public function testGetMoreTargetsThanExist(): void
{
$hashSpace = new Flexihash();
$hashSpace->addTarget('target1');
$hashSpace->addTarget('target2');
$targets = $hashSpace->lookupList('resource', 4);
$this->assertIsArray($targets);
$this->assertEquals(count($targets), 2);
$this->assertNotEquals($targets[0], $targets[1]);
}
public function testGetMultipleTargetsNeedingToLoopToStart(): void
{
$mockHasher = new MockHasher();
$hashSpace = new Flexihash($mockHasher, 1);
$mockHasher->setHashValue(10);
$hashSpace->addTarget('t1');
$mockHasher->setHashValue(20);
$hashSpace->addTarget('t2');
$mockHasher->setHashValue(30);
$hashSpace->addTarget('t3');
$mockHasher->setHashValue(40);
$hashSpace->addTarget('t4');
$mockHasher->setHashValue(50);
$hashSpace->addTarget('t5');
$mockHasher->setHashValue(35);
$targets = $hashSpace->lookupList('resource', 4);
$this->assertEquals($targets, ['t4', 't5', 't1', 't2']);
}
public function testGetMultipleTargetsWithoutGettingAnyBeforeLoopToStart(): void
{
$mockHasher = new MockHasher();
$hashSpace = new Flexihash($mockHasher, 1);
$mockHasher->setHashValue(10);
$hashSpace->addTarget('t1');
$mockHasher->setHashValue(20);
$hashSpace->addTarget('t2');
$mockHasher->setHashValue(30);
$hashSpace->addTarget('t3');
$mockHasher->setHashValue(100);
$targets = $hashSpace->lookupList('resource', 2);
$this->assertEquals($targets, ['t1', 't2']);
}
public function testGetMultipleTargetsWithoutNeedingToLoopToStart(): void
{
$mockHasher = new MockHasher();
$hashSpace = new Flexihash($mockHasher, 1);
$mockHasher->setHashValue(10);
$hashSpace->addTarget('t1');
$mockHasher->setHashValue(20);
$hashSpace->addTarget('t2');
$mockHasher->setHashValue(30);
$hashSpace->addTarget('t3');
$mockHasher->setHashValue(15);
$targets = $hashSpace->lookupList('resource', 2);
$this->assertEquals($targets, ['t2', 't3']);
}
public function testFallbackPrecedenceWhenServerRemoved(): void
{
$mockHasher = new MockHasher();
$hashSpace = new Flexihash($mockHasher, 1);
$mockHasher->setHashValue(10);
$hashSpace->addTarget('t1');
$mockHasher->setHashValue(20);
$hashSpace->addTarget('t2');
$mockHasher->setHashValue(30);
$hashSpace->addTarget('t3');
$mockHasher->setHashValue(15);
$this->assertEquals($hashSpace->lookup('resource'), 't2');
$this->assertEquals(
$hashSpace->lookupList('resource', 3),
['t2', 't3', 't1']
);
$hashSpace->removeTarget('t2');
$this->assertEquals($hashSpace->lookup('resource'), 't3');
$this->assertEquals(
$hashSpace->lookupList('resource', 3),
['t3', 't1']
);
$hashSpace->removeTarget('t3');
$this->assertEquals($hashSpace->lookup('resource'), 't1');
$this->assertEquals(
$hashSpace->lookupList('resource', 3),
['t1']
);
}
/**
* Does the __toString method behave as we expect.
*
* @author Dom Morgan
*/
public function testHashSpaceToString(): void
{
$mockHasher = new MockHasher();
$hashSpace = new Flexihash($mockHasher, 1);
$hashSpace->addTarget('t1');
$hashSpace->addTarget('t2');
$this->assertSame(
$hashSpace->__toString(),
'Flexihash\Flexihash{targets:[t1,t2]}'
);
}
}
================================================
FILE: tests/Hasher/HasherTest.php
================================================
hash('test');
$result2 = $hasher->hash('test');
$result3 = $hasher->hash('different');
$this->assertEquals($result1, $result2);
$this->assertNotEquals($result1, $result3); // fragile but worthwhile
}
public function testMd5Hash(): void
{
$hasher = new Md5Hasher();
$result1 = $hasher->hash('test');
$result2 = $hasher->hash('test');
$result3 = $hasher->hash('different');
$this->assertEquals($result1, $result2);
$this->assertNotEquals($result1, $result3); // fragile but worthwhile
}
}
================================================
FILE: tests/Hasher/MockHasher.php
================================================
hashValue = $hash;
}
public function hash($value)
{
return $this->hashValue;
}
}