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
================================================
<?xml version="1.0"?>
<ruleset name="Flexihash">
<description>Flexihash Coding Standard</description>
<rule ref="PSR2">
</rule>
</ruleset>
================================================
FILE: phpunit.xml.dist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.2/phpunit.xsd"
backupGlobals="false"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
bootstrap="vendor/autoload.php"
stopOnFailure="false">
<testsuites>
<testsuite name="Flexihash Test Suite">
<directory>tests</directory>
<exclude>tests/BenchmarkTest.php</exclude>
</testsuite>
</testsuites>
</phpunit>
================================================
FILE: src/Exception.php
================================================
<?php
declare(strict_types=1);
namespace Flexihash;
/**
* An exception thrown by Flexihash.
*
* @author Paul Annesley
* @license http://www.opensource.org/licenses/mit-license.php
*/
class Exception extends \Exception
{
}
================================================
FILE: src/Flexihash.php
================================================
<?php
declare(strict_types=1);
namespace Flexihash;
use Flexihash\Hasher\HasherInterface;
use Flexihash\Hasher\Crc32Hasher;
/**
* A simple consistent hashing implementation with pluggable hash algorithms.
*
* @author Paul Annesley
* @license 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 = [];
/**
* 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
================================================
<?php
declare(strict_types=1);
namespace Flexihash\Hasher;
/**
* Uses CRC32 to hash a value into a signed 32bit int address space.
* Under 32bit PHP this (safely) overflows into negatives ints.
*
* @author Paul Annesley
* @license http://www.opensource.org/licenses/mit-license.php
*/
class Crc32Hasher implements HasherInterface
{
public function hash($string): int
{
return crc32($string);
}
}
================================================
FILE: src/Hasher/HasherInterface.php
================================================
<?php
declare(strict_types=1);
namespace Flexihash\Hasher;
/**
* Hashes given values into a sortable fixed size address space.
*
* @author Paul Annesley
* @license http://www.opensource.org/licenses/mit-license.php
*/
interface HasherInterface
{
/**
* Hashes the given string into a 32bit address space.
*
* The data must have 0xFFFFFFFF possible values, and be sortable by
* PHP sort functions using SORT_REGULAR.
*
* @param string
* @return mixed A sortable format with 0xFFFFFFFF possible values
*/
public function hash($string);
}
================================================
FILE: src/Hasher/Md5Hasher.php
================================================
<?php
declare(strict_types=1);
namespace Flexihash\Hasher;
/**
* Uses MD5 to hash a value into a 32bit int.
*
* @author Paul Annesley
* @license http://www.opensource.org/licenses/mit-license.php
*/
class Md5Hasher implements HasherInterface
{
/**
* {@inheritDoc}
*
* 8 hexits = 32bit, which also allows us to forego having to check whether
* it's over PHP_INT_MAX.
*
* The substring is converted to an int since hex strings sometimes get
* treated as ints if all digits are ints and this results in unexpected
* sorting order.
*
* @param string $string
* @return int | float
* @author Dom Morgan <dom@d3r.com>
*/
public function hash($string)
{
return hexdec(substr(md5($string), 0, 8));
}
}
================================================
FILE: tests/BenchmarkTest.php
================================================
<?php
declare(strict_types=1);
namespace Flexihash\Tests;
use Flexihash\Flexihash;
use Flexihash\Hasher\Crc32Hasher;
use Flexihash\Hasher\Md5Hasher;
/**
* Benchmarks, not really tests.
*
* @author Paul Annesley
* @group benchmark
* @license http://www.opensource.org/licenses/mit-license.php
*/
class BenchmarkTest extends \PHPUnit\Framework\TestCase
{
private $targets = 10;
private $lookups = 1000;
public function dump($message): void
{
echo $message."\n";
}
public function testAddTargetWithNonConsistentHash(): 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", 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
================================================
<?php
declare(strict_types=1);
namespace Flexihash\Tests;
use Flexihash\Exception;
use Flexihash\Flexihash;
use Flexihash\Tests\Hasher\MockHasher;
/**
* @author Paul Annesley
* @license http://www.opensource.org/licenses/mit-license.php
*/
class FlexihashTest extends \PHPUnit\Framework\TestCase
{
public function testGetAllTargetsEmpty(): void
{
$hashSpace = new Flexihash();
$this->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 <dom@d3r.com>
*/
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
================================================
<?php
declare(strict_types=1);
namespace Flexihash\Tests\Hasher;
use Flexihash\Hasher\Crc32Hasher;
use Flexihash\Hasher\Md5Hasher;
/**
* @author Paul Annesley
* @license http://www.opensource.org/licenses/mit-license.php
*/
class HasherTest extends \PHPUnit\Framework\TestCase
{
public function testCrc32Hash(): void
{
$hasher = new Crc32Hasher();
$result1 = $hasher->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
================================================
<?php
declare(strict_types=1);
namespace Flexihash\Tests\Hasher;
use Flexihash\Hasher\HasherInterface;
/**
* @author Paul Annesley
* @license http://www.opensource.org/licenses/mit-license.php
*/
class MockHasher implements HasherInterface
{
private $hashValue;
public function setHashValue($hash): void
{
$this->hashValue = $hash;
}
public function hash($value)
{
return $this->hashValue;
}
}
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
SYMBOL INDEX (55 symbols across 9 files)
FILE: src/Exception.php
class Exception (line 12) | class Exception extends \Exception
FILE: src/Flexihash.php
class Flexihash (line 15) | class Flexihash
method __construct (line 73) | public function __construct(HasherInterface $hasher = null, $replicas ...
method addTarget (line 87) | public function addTarget($target, $weight = 1)
method addTargets (line 115) | public function addTargets($targets, $weight = 1)
method removeTarget (line 131) | public function removeTarget($target)
method getAllTargets (line 153) | public function getAllTargets(): array
method lookup (line 164) | public function lookup($resource): string
method lookupList (line 183) | public function lookupList($resource, $requestedCount): array
method __toString (line 245) | public function __toString(): string
method sortPositionTargets (line 260) | private function sortPositionTargets()
FILE: src/Hasher/Crc32Hasher.php
class Crc32Hasher (line 13) | class Crc32Hasher implements HasherInterface
method hash (line 15) | public function hash($string): int
FILE: src/Hasher/HasherInterface.php
type HasherInterface (line 12) | interface HasherInterface
method hash (line 23) | public function hash($string);
FILE: src/Hasher/Md5Hasher.php
class Md5Hasher (line 12) | class Md5Hasher implements HasherInterface
method hash (line 28) | public function hash($string)
FILE: tests/BenchmarkTest.php
class BenchmarkTest (line 17) | class BenchmarkTest extends \PHPUnit\Framework\TestCase
method dump (line 22) | public function dump($message): void
method testAddTargetWithNonConsistentHash (line 27) | public function testAddTargetWithNonConsistentHash(): void
method testRemoveTargetWithNonConsistentHash (line 52) | public function testRemoveTargetWithNonConsistentHash(): void
method testHopeAddingTargetDoesNotChangeMuchWithCrc32Hasher (line 77) | public function testHopeAddingTargetDoesNotChangeMuchWithCrc32Hasher()...
method testHopeRemovingTargetDoesNotChangeMuchWithCrc32Hasher (line 111) | public function testHopeRemovingTargetDoesNotChangeMuchWithCrc32Hasher...
method testHashDistributionWithCrc32Hasher (line 145) | public function testHashDistributionWithCrc32Hasher(): void
method testHasherSpeed (line 175) | public function testHasherSpeed(): void
method basicHash (line 204) | private function basicHash($value, $targets):int
method median (line 213) | private function median($values):int
FILE: tests/FlexihashTest.php
class FlexihashTest (line 14) | class FlexihashTest extends \PHPUnit\Framework\TestCase
method testGetAllTargetsEmpty (line 16) | public function testGetAllTargetsEmpty(): void
method testAddTargetThrowsExceptionOnDuplicateTarget (line 22) | public function testAddTargetThrowsExceptionOnDuplicateTarget(): void
method testAddTargetAndGetAllTargets (line 30) | public function testAddTargetAndGetAllTargets(): void
method testAddTargetsAndGetAllTargets (line 42) | public function testAddTargetsAndGetAllTargets(): void
method testRemoveTarget (line 51) | public function testRemoveTarget(): void
method testRemoveTargetFailsOnMissingTarget (line 63) | public function testRemoveTargetFailsOnMissingTarget(): void
method testHashSpaceRepeatableLookups (line 70) | public function testHashSpaceRepeatableLookups(): void
method testHashSpaceLookupListEmpty (line 81) | public function testHashSpaceLookupListEmpty(): void
method testHashSpaceLookupListNoTargets (line 87) | public function testHashSpaceLookupListNoTargets(): void
method testHashSpaceLookupListNo (line 95) | public function testHashSpaceLookupListNo(): void
method testHashSpaceLookupsAreValidTargets (line 103) | public function testHashSpaceLookupsAreValidTargets(): void
method testHashSpaceConsistentLookupsAfterAddingAndRemoving (line 121) | public function testHashSpaceConsistentLookupsAfterAddingAndRemoving()...
method testHashSpaceConsistentLookupsWithNewInstance (line 150) | public function testHashSpaceConsistentLookupsWithNewInstance(): void
method testGetMultipleTargets (line 173) | public function testGetMultipleTargets(): void
method testGetMultipleTargetsWithOnlyOneTarget (line 187) | public function testGetMultipleTargetsWithOnlyOneTarget(): void
method testGetMoreTargetsThanExist (line 199) | public function testGetMoreTargetsThanExist(): void
method testGetMultipleTargetsNeedingToLoopToStart (line 212) | public function testGetMultipleTargetsNeedingToLoopToStart(): void
method testGetMultipleTargetsWithoutGettingAnyBeforeLoopToStart (line 238) | public function testGetMultipleTargetsWithoutGettingAnyBeforeLoopToSta...
method testGetMultipleTargetsWithoutNeedingToLoopToStart (line 258) | public function testGetMultipleTargetsWithoutNeedingToLoopToStart(): void
method testFallbackPrecedenceWhenServerRemoved (line 278) | public function testFallbackPrecedenceWhenServerRemoved(): void
method testHashSpaceToString (line 322) | public function testHashSpaceToString(): void
FILE: tests/Hasher/HasherTest.php
class HasherTest (line 13) | class HasherTest extends \PHPUnit\Framework\TestCase
method testCrc32Hash (line 15) | public function testCrc32Hash(): void
method testMd5Hash (line 26) | public function testMd5Hash(): void
FILE: tests/Hasher/MockHasher.php
class MockHasher (line 12) | class MockHasher implements HasherInterface
method setHashValue (line 16) | public function setHashValue($hash): void
method hash (line 21) | public function hash($value)
Condensed preview — 19 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (37K chars).
[
{
"path": ".coveralls.yml",
"chars": 61,
"preview": "coverage_clover: clover.xml\njson_path: coveralls-upload.json\n"
},
{
"path": ".gitignore",
"chars": 9,
"preview": "/vendor/\n"
},
{
"path": ".travis.yml",
"chars": 782,
"preview": "language: php\n\nenv:\n global:\n - COVERALLS=0\n - PHPCS=0\n\nmatrix:\n include:\n - php: 7.2\n - php: 7."
},
{
"path": "CHANGELOG.md",
"chars": 1582,
"preview": "# Change Log\nAll notable changes to this project will be documented in this file\nwhich adheres to the guidelines at http"
},
{
"path": "LICENCE",
"chars": 1075,
"preview": "The MIT License\n\nCopyright (c) 2008 Paul Annesley\n\nPermission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "README.md",
"chars": 1733,
"preview": "# Flexihash\n\n[](https://travis-ci.org/pda/flexihas"
},
{
"path": "ROADMAP.md",
"chars": 503,
"preview": "#Roadmap\n\n## v1.0.0\n\nThis maintains the historical API but allows for composer autoloading.\n\n- [x] Composer support.\n- ["
},
{
"path": "composer.json",
"chars": 1106,
"preview": "{\n \"name\": \"flexihash/flexihash\",\n \"type\": \"library\",\n \"description\": \"Flexihash is a small PHP library which i"
},
{
"path": "phpcs.xml",
"chars": 142,
"preview": "<?xml version=\"1.0\"?>\n<ruleset name=\"Flexihash\">\n <description>Flexihash Coding Standard</description>\n <rule ref=\"PSR2\""
},
{
"path": "phpunit.xml.dist",
"chars": 631,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n xsi:noNa"
},
{
"path": "src/Exception.php",
"chars": 229,
"preview": "<?php\ndeclare(strict_types=1);\n\nnamespace Flexihash;\n\n/**\n * An exception thrown by Flexihash.\n *\n * @author Paul Annesl"
},
{
"path": "src/Flexihash.php",
"chars": 7483,
"preview": "<?php\ndeclare(strict_types=1);\n\nnamespace Flexihash;\n\nuse Flexihash\\Hasher\\HasherInterface;\nuse Flexihash\\Hasher\\Crc32Ha"
},
{
"path": "src/Hasher/Crc32Hasher.php",
"chars": 424,
"preview": "<?php\ndeclare(strict_types=1);\n\nnamespace Flexihash\\Hasher;\n\n/**\n * Uses CRC32 to hash a value into a signed 32bit int a"
},
{
"path": "src/Hasher/HasherInterface.php",
"chars": 590,
"preview": "<?php\ndeclare(strict_types=1);\n\nnamespace Flexihash\\Hasher;\n\n/**\n * Hashes given values into a sortable fixed size addre"
},
{
"path": "src/Hasher/Md5Hasher.php",
"chars": 790,
"preview": "<?php\ndeclare(strict_types=1);\n\nnamespace Flexihash\\Hasher;\n\n/**\n * Uses MD5 to hash a value into a 32bit int.\n *\n * @au"
},
{
"path": "tests/BenchmarkTest.php",
"chars": 6247,
"preview": "<?php\ndeclare(strict_types=1);\n\nnamespace Flexihash\\Tests;\n\nuse Flexihash\\Flexihash;\nuse Flexihash\\Hasher\\Crc32Hasher;\nu"
},
{
"path": "tests/FlexihashTest.php",
"chars": 9538,
"preview": "<?php\ndeclare(strict_types=1);\n\nnamespace Flexihash\\Tests;\n\nuse Flexihash\\Exception;\nuse Flexihash\\Flexihash;\nuse Flexih"
},
{
"path": "tests/Hasher/HasherTest.php",
"chars": 984,
"preview": "<?php\ndeclare(strict_types=1);\n\nnamespace Flexihash\\Tests\\Hasher;\n\nuse Flexihash\\Hasher\\Crc32Hasher;\nuse Flexihash\\Hashe"
},
{
"path": "tests/Hasher/MockHasher.php",
"chars": 446,
"preview": "<?php\ndeclare(strict_types=1);\n\nnamespace Flexihash\\Tests\\Hasher;\n\nuse Flexihash\\Hasher\\HasherInterface;\n\n/**\n * @author"
}
]
About this extraction
This page contains the full source code of the pda/flexihash GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 19 files (33.5 KB), approximately 9.5k tokens, and a symbol index with 55 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.