-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
15 changed files
with
693 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
name: PHP Multi-probe Consistent Hashing build | ||
on: | ||
push: | ||
branches: [ "main" ] | ||
pull_request: | ||
branches: [ "main" ] | ||
jobs: | ||
build: | ||
runs-on: ubuntu-latest | ||
strategy: | ||
matrix: | ||
php: | ||
- "8.2" | ||
- "8.3" | ||
dependencies: | ||
- "lowest" | ||
- "highest" | ||
include: | ||
- php-version: "8.3" | ||
composer-options: "--ignore-platform-reqs" | ||
steps: | ||
- name: Checkout | ||
uses: actions/checkout@v4 | ||
- name: Install PHP | ||
uses: "shivammathur/setup-php@v2" | ||
with: | ||
php-version: "${{ matrix.php }}" | ||
- name: Composer install | ||
uses: "ramsey/composer-install@v3" | ||
with: | ||
dependency-versions: "${{ matrix.dependencies }}" | ||
composer-options: "${{ matrix.composer-options }}" | ||
|
||
- name: Run tests | ||
run: composer test | ||
|
||
- name: PHPStan static analysis | ||
run: composer phpstan | ||
|
||
- name: Code Coverage Report | ||
uses: irongut/[email protected] | ||
with: | ||
filename: cobertura.xml | ||
badge: true | ||
fail_below_min: true | ||
format: markdown | ||
hide_branch_rate: false | ||
hide_complexity: true | ||
indicators: true | ||
output: both | ||
thresholds: '60 80' | ||
|
||
- name: Add Coverage PR Comment | ||
uses: marocchino/sticky-pull-request-comment@v2 | ||
if: github.event_name == 'pull_request' | ||
with: | ||
recreate: true | ||
path: code-coverage-results.md |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
composer.phar | ||
composer.lock | ||
/vendor/ | ||
|
||
# Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control | ||
# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file | ||
# composer.lock | ||
/build/ | ||
.idea | ||
.phpunit.cache | ||
clover.xml |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
<component name="ProjectRunConfigurationManager"> | ||
<configuration default="false" name="PHPUnit" type="PHPUnitRunConfigurationType" factoryName="PHPUnit"> | ||
<CommandLine> | ||
<PhpTestInterpreterSettings> | ||
<option name="interpreterName" value="PHP 8.3" /> | ||
</PhpTestInterpreterSettings> | ||
</CommandLine> | ||
<TestRunner configuration_file="$PROJECT_DIR$/phpunit.xml" coverage_engine="PCov" scope="XML" use_alternative_configuration_file="true" /> | ||
<method v="2" /> | ||
</configuration> | ||
</component> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,40 @@ | ||
# php-consistent-hashing | ||
Multi-probe consistent hashing, using php | ||
|
||
The idea behind this package is that you can distribute keys to nodes in a consistent/sticky way, while also allowing for custom weights for each node. | ||
This allows you to route 'traffic' to nodes depending on an equal or custom distibution. | ||
|
||
The algorithm should distribute more evenly than for example libketama, which can have a deviation between distributions of equal weight as big as about 10%. | ||
For more information on this algorithm and consistent hashing in general, see: | ||
- | ||
- https://en.wikipedia.org/wiki/Consistent_hashing | ||
|
||
Use cases: | ||
- Load balancing. | ||
- Distributing users to groups, for example for use in A/B testing. | ||
|
||
Example usage: | ||
```php | ||
$hasher = new \Jspeedz\PhpConsistentHashing\MultiProbeConsistentHash(); | ||
// Choose which hash functions to use | ||
$hasher->setHashFunctions((new \Jspeedz\PhpConsistentHashing\HashFunctions\Standard)()); | ||
``` | ||
|
||
Usage with equal weights: | ||
```php | ||
$hasher->addNode('node1'); | ||
$hasher->addNode('node2'); | ||
$hasher->addNode('node2'); | ||
|
||
$node = $hasher->hash('some_key1'); | ||
``` | ||
|
||
Usage with custom weights (I like to use percentages): | ||
```php | ||
$hasher->addNode('node1', 25); | ||
$hasher->addNode('node2', 33.3); | ||
$hasher->addNode('node2', 41.7); | ||
|
||
$node = $hasher->hash('some_key1'); | ||
``` | ||
Note: if you add a node without specifying a weight, the weight will be set to 1. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
{ | ||
"name": "jspeedz/php-consistent-hashing", | ||
"description": "Multi-probe consistent hashing implementation for PHP", | ||
"type": "library", | ||
"require-dev": { | ||
"phpunit/phpunit": "^11.2", | ||
"phpstan/phpstan": "^1.11" | ||
}, | ||
"license": "GPL-3.0-only", | ||
"authors": [ | ||
{ | ||
"name": "jspeedz" | ||
} | ||
], | ||
"autoload": { | ||
"psr-4": { | ||
"Jspeedz\\PhpConsistentHashing\\": "src/" | ||
} | ||
}, | ||
"scripts": { | ||
"generate-test-data": "@php tests/data/generatedata.php", | ||
"test": "@php vendor/bin/phpunit", | ||
"phpstan": "@php vendor/bin/phpstan analyse", | ||
"phpstanpro": "@php vendor/bin/phpstan --pro" | ||
}, | ||
"scripts-descriptions": { | ||
"generate-test-data": "Re-generate test data for tests", | ||
"test": "Run all tests", | ||
"phpstan": "Runs PHPStan", | ||
"phpstanpro": "Runs PHPStan in PRO mode!" | ||
}, | ||
"require": { | ||
"php": ">=8.2" | ||
}, | ||
"config": { | ||
"process-timeout": 0 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
parameters: | ||
level: 9 | ||
paths: | ||
- src | ||
- tests |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.2/phpunit.xsd" | ||
bootstrap="vendor/autoload.php" | ||
cacheDirectory=".phpunit.cache" | ||
executionOrder="depends,defects" | ||
requireCoverageMetadata="true" | ||
beStrictAboutCoverageMetadata="true" | ||
beStrictAboutOutputDuringTests="true" | ||
failOnRisky="true" | ||
failOnWarning="true"> | ||
<testsuites> | ||
<testsuite name="default"> | ||
<directory>tests</directory> | ||
</testsuite> | ||
</testsuites> | ||
|
||
<source ignoreIndirectDeprecations="true" restrictNotices="true" restrictWarnings="true"> | ||
<include> | ||
<directory>src</directory> | ||
</include> | ||
</source> | ||
|
||
<coverage includeUncoveredFiles="true" | ||
pathCoverage="false" | ||
ignoreDeprecatedCodeUnits="true" | ||
disableCodeCoverageIgnore="true"> | ||
<report> | ||
<cobertura outputFile="cobertura.xml"/> | ||
<clover outputFile="clover.xml"/> | ||
</report> | ||
</coverage> | ||
</phpunit> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace Jspeedz\PhpConsistentHashing\HashFunctions; | ||
|
||
class Standard { | ||
/** | ||
* @return callable[] | ||
*/ | ||
public function __invoke(): array { | ||
return [ | ||
function(string $key): int { | ||
return crc32($key); | ||
}, | ||
function(string $key): float|int { | ||
return hexdec(substr(hash('md5', $key), 0, 8)); | ||
}, | ||
function(string $key): float|int { | ||
return hexdec(substr(hash('sha256', $key), 0, 8)); | ||
}, | ||
]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace Jspeedz\PhpConsistentHashing; | ||
|
||
class MultiProbeConsistentHash { | ||
/** | ||
* @var array<string, float> $nodes | ||
*/ | ||
private array $nodes = []; | ||
private float $totalWeight = 0; | ||
private int $probeCount; | ||
/** | ||
* @var callable[] $hashFunctions | ||
*/ | ||
private array $hashFunctions; | ||
|
||
/** | ||
* @param callable[] $hashFunctions | ||
*/ | ||
public function setHashFunctions(array $hashFunctions): void { | ||
$this->probeCount = count($hashFunctions); | ||
$this->hashFunctions = array_values($hashFunctions); | ||
} | ||
|
||
public function addNode(?string $node, float $weight = 1): void { | ||
$this->nodes[$node] = $weight; | ||
$this->totalWeight += $weight; | ||
} | ||
|
||
public function removeNode(string $node): void { | ||
if(isset($this->nodes[$node])) { | ||
$this->totalWeight -= $this->nodes[$node]; | ||
unset($this->nodes[$node]); | ||
} | ||
} | ||
|
||
public function getNode(string $key): ?string { | ||
if(empty($this->nodes)) { | ||
return null; | ||
} | ||
|
||
$minHash = PHP_INT_MAX; | ||
$targetNode = null; | ||
|
||
foreach($this->nodes as $node => $weight) { | ||
for($i = 0; $i < $this->probeCount; $i++) { | ||
$hash = $this->hashFunctions[$i]($key . $node); | ||
// Adjust hash by weight | ||
$weightedHash = $hash / $weight; | ||
if($weightedHash < $minHash) { | ||
$minHash = $weightedHash; | ||
$targetNode = $node; | ||
} | ||
} | ||
} | ||
|
||
return $targetNode; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace Jspeedz\PhpConsistentHashing\Tests\HashFunctions; | ||
|
||
use Jspeedz\PhpConsistentHashing\HashFunctions\Standard; | ||
use PHPUnit\Framework\Attributes\CoversClass; | ||
use PHPUnit\Framework\TestCase; | ||
|
||
#[CoversClass(Standard::class)] | ||
class StandardTest extends TestCase { | ||
public function testStandardCallbacks(): void { | ||
$callbacks = (new Standard())(); | ||
|
||
$this->assertCount(3, $callbacks); | ||
|
||
$this->assertIsCallable($callbacks[0]); | ||
$this->assertIsCallable($callbacks[1]); | ||
$this->assertIsCallable($callbacks[2]); | ||
|
||
$this->assertSame( | ||
crc32('test'), | ||
$callbacks[0]('test'), | ||
); | ||
$this->assertSame( | ||
hexdec(substr(hash('md5', 'test'), 0, 8)), | ||
$callbacks[1]('test'), | ||
); | ||
$this->assertSame( | ||
hexdec(substr(hash('sha256', 'test'), 0, 8)), | ||
$callbacks[2]('test'), | ||
); | ||
} | ||
} |
Oops, something went wrong.