diff --git a/.gitattributes b/.gitattributes
index fa173dd..6fc21e5 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -15,3 +15,5 @@
/psalm-baseline.xml export-ignore
/psalm.xml export-ignore
/tests/ export-ignore
+/.Dockerfile export-ignore
+/.Makefile export-ignore
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index bdf2f73..6bb085d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -24,60 +24,48 @@ jobs:
parallel-lint:
name: 'ParallelLint'
runs-on: 'ubuntu-latest'
-
steps:
- name: Checkout repository
uses: actions/checkout@v3
-
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
- php-version: '8.1'
+ php-version: '8.2'
coverage: none
-
- name: Install dependencies
uses: ramsey/composer-install@v2
-
- name: Run ParallelLint
run: composer parallel-lint -- --no-progress --ignore-fails
psalm:
name: 'Psalm'
runs-on: 'ubuntu-latest'
-
steps:
- name: Checkout repository
uses: actions/checkout@v3
-
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
- php-version: '8.1'
+ php-version: '8.2'
coverage: none
-
- name: Install dependencies
uses: ramsey/composer-install@v2
-
- name: Run Psalm
run: composer psalm -- --no-progress --no-cache --output-format=github
php-cs-fixer:
name: 'PHPCsFixer'
runs-on: 'ubuntu-latest'
-
steps:
- name: Checkout repository
uses: actions/checkout@v3
-
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
- php-version: '8.1'
+ php-version: '8.2'
coverage: none
-
- name: Install dependencies
uses: ramsey/composer-install@v2
-
- name: Run PHPCsFixer
run: composer php-cs-fixer:diff -- --no-interaction --using-cache=no
@@ -85,7 +73,6 @@ jobs:
name: 'PHPUnit'
needs: ['parallel-lint', 'psalm', 'php-cs-fixer']
runs-on: ${{ matrix.operating-system }}
-
strategy:
fail-fast: false
matrix:
@@ -94,24 +81,21 @@ jobs:
- 'windows-latest'
php-version:
- '8.1'
+ - '8.2'
composer-dependency:
- 'lowest'
- 'highest'
-
steps:
- name: Checkout repository
uses: actions/checkout@v3
-
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
coverage: none
-
- name: Install dependencies
uses: ramsey/composer-install@v2
with:
dependency-versions: ${{ matrix.composer-dependency }}
-
- name: Run PHPUnit
run: composer phpunit -- --no-interaction --do-not-cache-result
diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php
index fb017ec..790a992 100644
--- a/.php-cs-fixer.dist.php
+++ b/.php-cs-fixer.dist.php
@@ -17,6 +17,8 @@
->setRules([
'@Symfony' => true,
'@Symfony:risky' => true,
+ 'phpdoc_separation' => false,
+ 'phpdoc_to_comment' => false,
])
->setFinder($finder)
;
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..f6541a1
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,11 @@
+FROM php:8.2-cli
+
+RUN \
+ apt-get update ; \
+ apt-get install -y unzip ; \
+ pecl install pcov ; \
+ docker-php-ext-enable pcov ;
+
+COPY --from=composer:2.4 /usr/bin/composer /usr/local/bin/composer
+
+WORKDIR /usr/local/packages/throttler/
diff --git a/LICENSE b/LICENSE
index 880306b..cb8d327 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2022 Orangesoft
+Copyright (c) 2021 Orangesoft
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..42539b6
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,5 @@
+init:
+ docker build -t throttler:8.2 ./
+
+exec:
+ docker run --name throttler --rm --interactive --tty --volume ${PWD}:/usr/local/packages/throttler/ throttler:8.2 /bin/bash
diff --git a/README.md b/README.md
index 4344d00..87664ba 100644
--- a/README.md
+++ b/README.md
@@ -20,38 +20,59 @@ This package requires PHP 8.1 or later.
## Quick usage
-Configure Throttler as below:
+Configure `Orangesoft\Throttler\WeightedRoundRobinThrottler::class` as below and set weight for each node if you are using weighted strategy:
```php
pick($collection);
-
+
// ...
}
```
-Set weight for Node as the second argument in constructor if you are using weighted-strategies.
+As a result, the throttler will go through all the nodes and return the appropriate one according to the chosen strategy as shown below:
+
+```text
++---------+-------------+
+| request | node |
++---------+-------------+
+| 1 | 192.168.0.1 |
+| 2 | 192.168.0.1 |
+| 3 | 192.168.0.1 |
+| 4 | 192.168.0.1 |
+| 5 | 192.168.0.1 |
+| 6 | 192.168.0.2 |
+| 7 | 192.168.0.3 |
+| n | etc. |
++---------+-------------+
+```
+
+The following throttlers are available:
+
+- [Orangesoft\Throttler\RandomThrottler](./src/RandomThrottler.php)
+- [Orangesoft\Throttler\WeightedRandomThrottler](./src/WeightedRandomThrottler.php)
+- [Orangesoft\Throttler\FrequencyRandomThrottler](./src/FrequencyRandomThrottler.php)
+- [Orangesoft\Throttler\RoundRobinThrottler](./src/RoundRobinThrottler.php)
+- [Orangesoft\Throttler\WeightedRoundRobinThrottler](./src/WeightedRoundRobinThrottler.php)
+- [Orangesoft\Throttler\SmoothWeightedRoundRobinThrottler](./src/SmoothWeightedRoundRobinThrottler.php)
## Benchmarks
@@ -61,25 +82,25 @@ Run `composer phpbench` to check out benchmarks:
+-------------------------------+------+-----+----------+----------+----------+---------+
| benchmark | revs | its | mean | best | worst | stdev |
+-------------------------------+------+-----+----------+----------+----------+---------+
-| FrequencyRandomBench | 1000 | 5 | 6.074μs | 5.924μs | 6.242μs | 0.139μs |
| RandomBench | 1000 | 5 | 4.002μs | 3.880μs | 4.097μs | 0.073μs |
-| RoundRobinBench | 1000 | 5 | 4.060μs | 3.888μs | 4.363μs | 0.171μs |
-| SmoothWeightedRoundRobinBench | 1000 | 5 | 6.888μs | 6.707μs | 7.102μs | 0.130μs |
| WeightedRandomBench | 1000 | 5 | 11.660μs | 11.533μs | 11.797μs | 0.094μs |
+| FrequencyRandomBench | 1000 | 5 | 6.074μs | 5.924μs | 6.242μs | 0.139μs |
+| RoundRobinBench | 1000 | 5 | 4.060μs | 3.888μs | 4.363μs | 0.171μs |
| WeightedRoundRobinBench | 1000 | 5 | 10.778μs | 10.655μs | 10.919μs | 0.115μs |
+| SmoothWeightedRoundRobinBench | 1000 | 5 | 6.888μs | 6.707μs | 7.102μs | 0.130μs |
+-------------------------------+------+-----+----------+----------+----------+---------+
```
-The report is based on measuring the speed. Check `best` column to find out which strategy is the fastest. You can see that the fastest strategies are Random and RoundRobin.
+The report is based on measuring the speed. Check `best` column to find out which strategy is the fastest.
## Documentation
-- [Configure Throttler](docs/index.md#configure-throttler)
-- [Available strategies](docs/index.md#available-strategies)
-- [Sort nodes](docs/index.md#sort-nodes)
-- [Keep counter](docs/index.md#keep-counter)
-- [Serialize strategies](docs/index.md#serialize-strategies)
-- [Dynamically change strategy](docs/index.md#dynamically-change-strategy)
-- [Balance cluster](docs/index.md#balance-cluster)
+- [Available strategies](./docs/index.md#available-strategies)
+- [Keep states](./docs/index.md#keep-states)
+- [Custom counter](./docs/index.md#custom-counter)
+- [Custom strategy](./docs/index.md#custom-strategy)
+- [Multiple throttler](./docs/index.md#multiple-throttler)
+- [Balance cluster](./docs/index.md#balance-cluster)
+- [Guzzle middleware](./docs/index.md#guzzle-middleware)
-Read more about usage on [Orangesoft Tech](https://orangesoft.co/blog/how-to-make-proxy-balancing-in-guzzle).
+Read more about load balancing on [Sam Rose's blog](https://samwho.dev/load-balancing/).
diff --git a/benchmarks/FrequencyRandomBench.php b/benchmarks/FrequencyRandomBench.php
index 4b7e47b..d7f9717 100644
--- a/benchmarks/FrequencyRandomBench.php
+++ b/benchmarks/FrequencyRandomBench.php
@@ -4,37 +4,33 @@
namespace Orangesoft\Throttler\Benchmarks;
-use Orangesoft\Throttler\Collection\Collection;
use Orangesoft\Throttler\Collection\CollectionInterface;
+use Orangesoft\Throttler\Collection\InMemoryCollection;
use Orangesoft\Throttler\Collection\Node;
-use Orangesoft\Throttler\Strategy\FrequencyRandomStrategy;
-use Orangesoft\Throttler\Throttler;
+use Orangesoft\Throttler\FrequencyRandomThrottler;
use Orangesoft\Throttler\ThrottlerInterface;
-class FrequencyRandomBench
+final class FrequencyRandomBench
{
private CollectionInterface $collection;
private ThrottlerInterface $throttler;
public function __construct()
{
- $this->collection = new Collection([
- new Node('node1'),
- new Node('node2'),
- new Node('node3'),
+ $this->collection = new InMemoryCollection([
+ new Node('192.168.0.1'),
+ new Node('192.168.0.2'),
+ new Node('192.168.0.3'),
]);
- $this->throttler = new Throttler(
- new FrequencyRandomStrategy(),
- );
+ $this->throttler = new FrequencyRandomThrottler();
}
/**
* @Revs(1000)
- *
* @Iterations(5)
*/
- public function benchFrequencyRandom(): void
+ public function benchFrequencyRandomAlgorithm(): void
{
$this->throttler->pick($this->collection);
}
diff --git a/benchmarks/RandomBench.php b/benchmarks/RandomBench.php
index 8ea5b62..571b5dd 100644
--- a/benchmarks/RandomBench.php
+++ b/benchmarks/RandomBench.php
@@ -4,37 +4,33 @@
namespace Orangesoft\Throttler\Benchmarks;
-use Orangesoft\Throttler\Collection\Collection;
use Orangesoft\Throttler\Collection\CollectionInterface;
+use Orangesoft\Throttler\Collection\InMemoryCollection;
use Orangesoft\Throttler\Collection\Node;
-use Orangesoft\Throttler\Strategy\RandomStrategy;
-use Orangesoft\Throttler\Throttler;
+use Orangesoft\Throttler\RandomThrottler;
use Orangesoft\Throttler\ThrottlerInterface;
-class RandomBench
+final class RandomBench
{
private CollectionInterface $collection;
private ThrottlerInterface $throttler;
public function __construct()
{
- $this->collection = new Collection([
- new Node('node1'),
- new Node('node2'),
- new Node('node3'),
+ $this->collection = new InMemoryCollection([
+ new Node('192.168.0.1'),
+ new Node('192.168.0.2'),
+ new Node('192.168.0.3'),
]);
- $this->throttler = new Throttler(
- new RandomStrategy(),
- );
+ $this->throttler = new RandomThrottler();
}
/**
* @Revs(1000)
- *
* @Iterations(5)
*/
- public function benchRandom(): void
+ public function benchRandomAlgorithm(): void
{
$this->throttler->pick($this->collection);
}
diff --git a/benchmarks/RoundRobinBench.php b/benchmarks/RoundRobinBench.php
index a6deaa8..f4fde43 100644
--- a/benchmarks/RoundRobinBench.php
+++ b/benchmarks/RoundRobinBench.php
@@ -4,40 +4,34 @@
namespace Orangesoft\Throttler\Benchmarks;
-use Orangesoft\Throttler\Collection\Collection;
use Orangesoft\Throttler\Collection\CollectionInterface;
+use Orangesoft\Throttler\Collection\InMemoryCollection;
use Orangesoft\Throttler\Collection\Node;
-use Orangesoft\Throttler\Strategy\InMemoryCounter;
-use Orangesoft\Throttler\Strategy\RoundRobinStrategy;
-use Orangesoft\Throttler\Throttler;
+use Orangesoft\Throttler\Counter\InMemoryCounter;
+use Orangesoft\Throttler\RoundRobinThrottler;
use Orangesoft\Throttler\ThrottlerInterface;
-class RoundRobinBench
+final class RoundRobinBench
{
private CollectionInterface $collection;
private ThrottlerInterface $throttler;
public function __construct()
{
- $this->collection = new Collection([
- new Node('node1'),
- new Node('node2'),
- new Node('node3'),
+ $this->collection = new InMemoryCollection([
+ new Node('192.168.0.1'),
+ new Node('192.168.0.2'),
+ new Node('192.168.0.3'),
]);
- $this->throttler = new Throttler(
- new RoundRobinStrategy(
- new InMemoryCounter(start: 0),
- )
- );
+ $this->throttler = new RoundRobinThrottler(new InMemoryCounter());
}
/**
* @Revs(1000)
- *
* @Iterations(5)
*/
- public function benchRoundRobin(): void
+ public function benchRoundRobinAlgorithm(): void
{
$this->throttler->pick($this->collection);
}
diff --git a/benchmarks/SmoothWeightedRoundRobinBench.php b/benchmarks/SmoothWeightedRoundRobinBench.php
index 896d7dc..148617c 100644
--- a/benchmarks/SmoothWeightedRoundRobinBench.php
+++ b/benchmarks/SmoothWeightedRoundRobinBench.php
@@ -4,37 +4,33 @@
namespace Orangesoft\Throttler\Benchmarks;
-use Orangesoft\Throttler\Collection\Collection;
use Orangesoft\Throttler\Collection\CollectionInterface;
+use Orangesoft\Throttler\Collection\InMemoryCollection;
use Orangesoft\Throttler\Collection\Node;
-use Orangesoft\Throttler\Strategy\SmoothWeightedRoundRobinStrategy;
-use Orangesoft\Throttler\Throttler;
+use Orangesoft\Throttler\SmoothWeightedRoundRobinThrottler;
use Orangesoft\Throttler\ThrottlerInterface;
-class SmoothWeightedRoundRobinBench
+final class SmoothWeightedRoundRobinBench
{
private CollectionInterface $collection;
private ThrottlerInterface $throttler;
public function __construct()
{
- $this->collection = new Collection([
- new Node('node1', 5),
- new Node('node2', 1),
- new Node('node3', 1),
+ $this->collection = new InMemoryCollection([
+ new Node('192.168.0.1', 5),
+ new Node('192.168.0.2', 1),
+ new Node('192.168.0.3', 1),
]);
- $this->throttler = new Throttler(
- new SmoothWeightedRoundRobinStrategy(),
- );
+ $this->throttler = new SmoothWeightedRoundRobinThrottler();
}
/**
* @Revs(1000)
- *
* @Iterations(5)
*/
- public function benchSmoothWeightedRoundRobin(): void
+ public function benchSmoothWeightedRoundRobinAlgorithm(): void
{
$this->throttler->pick($this->collection);
}
diff --git a/benchmarks/WeightedRandomBench.php b/benchmarks/WeightedRandomBench.php
index ac0a0a1..a242ca4 100644
--- a/benchmarks/WeightedRandomBench.php
+++ b/benchmarks/WeightedRandomBench.php
@@ -4,37 +4,33 @@
namespace Orangesoft\Throttler\Benchmarks;
-use Orangesoft\Throttler\Collection\Collection;
use Orangesoft\Throttler\Collection\CollectionInterface;
+use Orangesoft\Throttler\Collection\InMemoryCollection;
use Orangesoft\Throttler\Collection\Node;
-use Orangesoft\Throttler\Strategy\WeightedRandomStrategy;
-use Orangesoft\Throttler\Throttler;
use Orangesoft\Throttler\ThrottlerInterface;
+use Orangesoft\Throttler\WeightedRandomThrottler;
-class WeightedRandomBench
+final class WeightedRandomBench
{
private CollectionInterface $collection;
private ThrottlerInterface $throttler;
public function __construct()
{
- $this->collection = new Collection([
- new Node('node1', 5),
- new Node('node2', 1),
- new Node('node3', 1),
+ $this->collection = new InMemoryCollection([
+ new Node('192.168.0.1', 5),
+ new Node('192.168.0.2', 1),
+ new Node('192.168.0.3', 1),
]);
- $this->throttler = new Throttler(
- new WeightedRandomStrategy(),
- );
+ $this->throttler = new WeightedRandomThrottler();
}
/**
* @Revs(1000)
- *
* @Iterations(5)
*/
- public function benchWeightedRandom(): void
+ public function benchWeightedRandomAlgorithm(): void
{
$this->throttler->pick($this->collection);
}
diff --git a/benchmarks/WeightedRoundRobinBench.php b/benchmarks/WeightedRoundRobinBench.php
index 9a2ee6e..8b99314 100644
--- a/benchmarks/WeightedRoundRobinBench.php
+++ b/benchmarks/WeightedRoundRobinBench.php
@@ -4,40 +4,34 @@
namespace Orangesoft\Throttler\Benchmarks;
-use Orangesoft\Throttler\Collection\Collection;
use Orangesoft\Throttler\Collection\CollectionInterface;
+use Orangesoft\Throttler\Collection\InMemoryCollection;
use Orangesoft\Throttler\Collection\Node;
-use Orangesoft\Throttler\Strategy\InMemoryCounter;
-use Orangesoft\Throttler\Strategy\WeightedRoundRobinStrategy;
-use Orangesoft\Throttler\Throttler;
+use Orangesoft\Throttler\Counter\InMemoryCounter;
use Orangesoft\Throttler\ThrottlerInterface;
+use Orangesoft\Throttler\WeightedRoundRobinThrottler;
-class WeightedRoundRobinBench
+final class WeightedRoundRobinBench
{
private CollectionInterface $collection;
private ThrottlerInterface $throttler;
public function __construct()
{
- $this->collection = new Collection([
- new Node('node1', 5),
- new Node('node2', 1),
- new Node('node3', 1),
+ $this->collection = new InMemoryCollection([
+ new Node('192.168.0.1', 5),
+ new Node('192.168.0.2', 1),
+ new Node('192.168.0.3', 1),
]);
- $this->throttler = new Throttler(
- new WeightedRoundRobinStrategy(
- new InMemoryCounter(start: 0),
- )
- );
+ $this->throttler = new WeightedRoundRobinThrottler(new InMemoryCounter());
}
/**
* @Revs(1000)
- *
* @Iterations(5)
*/
- public function benchWeightedRoundRobin(): void
+ public function benchWeightedRoundRobinAlgorithm(): void
{
$this->throttler->pick($this->collection);
}
diff --git a/composer.json b/composer.json
index eb0a3d6..52ec204 100644
--- a/composer.json
+++ b/composer.json
@@ -38,7 +38,10 @@
"autoload": {
"psr-4": {
"Orangesoft\\Throttler\\": "./src/"
- }
+ },
+ "files": [
+ "./helpers.php"
+ ]
},
"autoload-dev": {
"psr-4": {
@@ -47,17 +50,20 @@
}
},
"scripts": {
- "phpbench": "vendor/bin/phpbench run --report=throttler --ansi",
+ "phpbench": "./vendor/bin/phpbench run --report=throttler --ansi",
"phpunit": "./vendor/bin/phpunit --verbose --colors=always --no-coverage",
+ "phpunit:clear-cache": "rm ./build/cache/phpunit.cache",
"phpunit-coverage": "./vendor/bin/phpunit --verbose --colors=always --coverage-text",
"phpunit-coverage-html": "./vendor/bin/phpunit --verbose --colors=always --coverage-html ./build/logs/phpunit-coverage/",
"parallel-lint": "./vendor/bin/parallel-lint --colors ./src/ ./tests/ ./benchmarks/",
"php-cs-fixer:fix": "./vendor/bin/php-cs-fixer fix --verbose --ansi --show-progress=dots",
"php-cs-fixer:diff": "./vendor/bin/php-cs-fixer fix --verbose --ansi --dry-run --diff",
+ "php-cs-fixer:clear-cache": "rm ./build/cache/php-cs-fixer.cache",
"psalm": "./vendor/bin/psalm --show-info=true",
- "psalm:set-baseline": "@psalm --set-baseline=./psalm-baseline.xml",
- "psalm:update-baseline": "@psalm --update-baseline",
- "psalm:ignore-baseline": "@psalm --ignore-baseline",
+ "psalm:clear-cache": "rm -rf ./build/cache/psalm/",
+ "psalm:set-baseline": "@psalm --set-baseline=./psalm-baseline.xml --no-cache",
+ "psalm:update-baseline": "@psalm --update-baseline --no-cache",
+ "psalm:ignore-baseline": "@psalm --ignore-baseline --no-cache",
"test": [
"@parallel-lint",
"@psalm",
diff --git a/docs/index.md b/docs/index.md
index e07aded..4b628cf 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,329 +1,707 @@
# Documentation
-- [Configure Throttler](#configure-throttler)
- [Available strategies](#available-strategies)
-- [Sort nodes](#sort-nodes)
-- [Keep counter](#keep-counter)
-- [Serialize strategies](#serialize-strategies)
-- [Dynamically change strategy](#dynamically-change-strategy)
+ - [Random](#random)
+ - [Weighted random](#weighted-random)
+ - [Frequency random](#frequency-random)
+ - [Round-robin](#round-robin)
+ - [Weighted round-robin](#weighted-round-robin)
+ - [Smooth weighted round-robin](#smooth-weighted-round-robin)
+- [Keep states](#keep-states)
+ - [Use counting](#use-counting)
+ - [Use serialization](#use-serialization)
+- [Custom counter](#custom-counter)
+- [Custom strategy](#custom-strategy)
+- [Multiple throttler](#multiple-throttler)
- [Balance cluster](#balance-cluster)
+- [Guzzle middleware](#guzzle-middleware)
-## Configure Throttler
+## Available strategies
+
+The following strategies are available:
-You need to choose a strategy and configure it:
+### Random
+
+Random is a strategy where each node has an equal probability of being chosen, regardless of previous selections or the order of nodes. Use [Orangesoft\Throttler\RandomThrottler](../src/RandomThrottler.php) as shown below:
```php
pick($collection);
+
+ // ...
+}
```
-To get next Node call the `next()` method with passed collection:
+See a visualization of the random strategy's output:
+
+```text
++---------+-------------+--------+
+| request | node | chance |
++---------+-------------+--------+
+| 1 | 192.168.0.1 | 25.0% |
+| 2 | 192.168.0.2 | 25.0% |
+| 3 | 192.168.0.3 | 25.0% |
+| 4 | 192.168.0.4 | 25.0% |
+| n | etc. | |
++---------+-------------+--------+
+```
+
+### Weighted random
+
+Weighted random is a sort of random strategy where the probability of selecting each node is proportional to its assigned weight, allowing some nodes to have a higher chance of being chosen than others. Use [Orangesoft\Throttler\WeightedRandomThrottler](../src/WeightedRandomThrottler.php) as shown below:
```php
+pick($collection);
-
+
// ...
}
```
-As a result, you will see the following distribution of nodes:
+See a visualization of the weighted random strategy's output:
```text
-+-------+
-| node1 |
-| node1 |
-| node1 |
-| node1 |
-| node1 |
-| node2 |
-| node3 |
-| etc. |
-+-------+
++---------+-------------+--------+
+| request | node | chance |
++---------+-------------+--------+
+| 1 | 192.168.0.1 | 62.5% |
+| 2 | 192.168.0.2 | 12.5% |
+| 3 | 192.168.0.3 | 12.5% |
+| 4 | 192.168.0.4 | 12.5% |
+| n | etc. | |
++---------+-------------+--------+
```
-Result of Throttler depends on the chosen strategy.
+### Frequency random
-## Available strategies
+Frequency random is a strategy that allows selecting nodes with a specific frequency for a certain depth of the collection. For example, a threshold of `0.2` represents 20% of the nodes from their total length, and a frequency of `0.8` means there's an 80% probability that the first 20% of nodes will be picked. Nodes are sorted by their weight or provided in the order they were added to the collection. Use [Orangesoft\Throttler\FrequencyRandomThrottler](../src/FrequencyRandomThrottler.php) as shown below:
-Strategies are divided into two types: random and round-robin. The following strategies are available:
+```php
+pick($collection);
-## Sort nodes
+ // ...
+}
+```
-For some strategies, such as [FrequencyRandomStrategy](../src/Strategy/FrequencyRandomStrategy.php), it might be necessary to adjust the order of nodes by their weight. This can be done with Sorter:
+See a visualization of the frequency random strategy's output:
+
+```text
++----------+--------------+--------+
+| request | node | chance |
++----------+--------------+--------+
+| 1 | 192.168.0.10 | 40.0% |
+| 2 | 192.168.0.9 | 40.9% |
++----------+--------------+--------+
+| 3 | 192.168.0.8 | 2.5% |
+| 4 | 192.168.0.7 | 2.5% |
+| 5 | 192.168.0.6 | 2.5% |
+| 6 | 192.168.0.5 | 2.5% |
+| 7 | 192.168.0.4 | 2.5% |
+| 8 | 192.168.0.3 | 2.5% |
+| 9 | 192.168.0.2 | 2.5% |
+| 10 | 192.168.0.1 | 2.5% |
+| n | etc. | |
++----------+--------------+--------+
+```
+
+### Round-robin
+
+Round-robin is a strategy in which nodes in a collection are processed cyclically and sequentially, with equal priority. Use [Orangesoft\Throttler\RoundRobinThrottler](../src/RoundRobinThrottler.php) as shown below:
```php
pick($collection);
-$sorter->sort($collection, new Desc());
+ // ...
+}
```
-The nodes at the top of the list will be used more often. You can manage sorting using [Asc](../src/Collection/Asc.php) and [Desc](../src/Collection/Desc.php) comparators. Example for the Desc direction:
+See a visualization of the round-robin strategy's output:
```text
-+--------+--------+
-| name | weight |
-+--------+--------+
-| node7 | 2048 |
-| node4 | 1024 |
-| node3 | 512 |
-| node8 | 256 |
-| node5 | 128 |
-| node6 | 64 |
-| node1 | 32 |
-| node2 | 16 |
-| node9 | 8 |
-| node10 | 4 |
-+--------+--------+
++---------+-------------+
+| request | node |
++---------+-------------+
+| 1 | 192.168.0.1 |
+| 2 | 192.168.0.2 |
+| 3 | 192.168.0.3 |
+| 4 | 192.168.0.4 |
+| n | etc. |
++---------+-------------+
```
-FrequencyRandomStrategy has 2 not required options: frequency and depth. Frequency is probability to choose nodes from a first group in percent. Depth is length the first group from the list in percent. By default, frequency is 0.8 and depth is 0.2:
+### Weighted round-robin
+
+Weighted round-robin is a modification of the round-robin strategy, where each node is assigned a weight that determines its priority or frequency of selection in the distribution cycle. Use [Orangesoft\Throttler\WeightedRoundRobinThrottler](../src/WeightedRoundRobinThrottler.php) as shown below:
```php
-$throttler = new Throttler(
- new FrequencyRandomStrategy(frequency: 0.8, depth: 0.2),
+pick($collection);
+$collection = new InMemoryCollection([
+ new Node('192.168.0.1', 5),
+ new Node('192.168.0.2', 1),
+ new Node('192.168.0.3', 1),
+ new Node('192.168.0.4', 1),
+]);
+
+while (true) {
+ /** @var NodeInterface $node */
+ $node = $throttler->pick($collection);
+
+ // ...
+}
```
-The probability of choosing nodes for FrequencyRandomStrategy can be visualized as follows:
+See a visualization of the weighted round-robin strategy's output:
```text
-+--------+--------+
-| nodes | chance |
-+--------+--------+
-| node7 | 40% |
-| node4 | 40% |
-+--------+--------+
-| node3 | 2.5% |
-| node8 | 2.5% |
-| node5 | 2.5% |
-| node6 | 2.5% |
-| node1 | 2.5% |
-| node2 | 2.5% |
-| node9 | 2.5% |
-| node10 | 2.5% |
-+--------+--------+
++---------+-------------+
+| request | node |
++---------+-------------+
+| 1 | 192.168.0.1 |
+| 2 | 192.168.0.1 |
+| 3 | 192.168.0.1 |
+| 4 | 192.168.0.1 |
+| 5 | 192.168.0.1 |
+| 6 | 192.168.0.2 |
+| 7 | 192.168.0.3 |
+| 8 | 192.168.0.4 |
+| n | etc. |
++---------+-------------+
```
-If you need the reverse order of the nodes use Asc direction.
-
-## Keep counter
+### Smooth weighted round-robin
-For strategies are [RoundRobinStrategy](../src/Strategy/RoundRobinStrategy.php) and [WeightedRoundRobinStrategy](../src/Strategy/WeightedRoundRobinStrategy.php) you must use InMemoryCounter to remember order of nodes. A counter is not needed for round-robin strategies:
+Smooth weighted round-robin is an improved version of weighted round-robin that provides a more even distribution of load among nodes with different weights, minimizing fluctuations in the selection of elements. Use [Orangesoft\Throttler\SmoothWeightedRoundRobinThrottler](../src/SmoothWeightedRoundRobinThrottler.php) as shown below:
```php
pick($collection);
+
+ // ...
+}
+```
+
+See a visualization of the smooth weighted round-robin strategy's output:
+
+```text
++---------+-------------+
+| request | node |
++---------+-------------+
+| 1 | 192.168.0.1 |
+| 2 | 192.168.0.1 |
+| 3 | 192.168.0.2 |
+| 4 | 192.168.0.1 |
+| 5 | 192.168.0.3 |
+| 6 | 192.168.0.1 |
+| 7 | 192.168.0.4 |
+| 8 | 192.168.0.1 |
+| n | etc. |
++---------+-------------+
+```
+
+## Keep states
+
+Load balancing strategies can be of 2 types: *random-based* and *round-robin based*. Random-based strategies don't support keeping states between calls in different processes, as each request is based on probability. Round-robin based strategies support keeping states through a counting or serialization:
+
+```text
++-----------------------------+---------------+
+| Strategy | Method |
++-----------------------------+---------------+
+| random | [x] |
+| weighted random | [x] |
+| frequency random | [x] |
+| round-robin | counting |
+| weighted round-robin | counting |
+| smooth weighted round-robin | serialization |
++-----------------------------+---------------+
```
-You can replace InMemoryCounter if you need to keep the order of these strategies between PHP calls, for example, in queues. Just to implement [CounterInterface](../src/Strategy/CounterInterface.php):
+This is especially useful when it's necessary to resume work precisely from where the previous process ended.
+
+### Use counting
+
+For round-robin and weighted round-robin strategies, the counter `Orangesoft\Throttler\Counter\InMemoryCounter::class` is available, which stores the request count in memory:
```php
-class RedisCounter implements CounterInterface
-{
- public function __construct(
- private Client $client,
- ) {
- }
+client->exists($name)) {
- $this->client->set($name, -1);
- }
+use Orangesoft\Throttler\RoundRobinThrottler;
+use Orangesoft\Throttler\Counter\InMemoryCounter;
+use Orangesoft\Throttler\Collection\InMemoryCollection;
- return $this->client->incr($name);
- }
+$counter = 0;
+
+$throttler = new RoundRobinThrottler(
+ new InMemoryCounter(
+ start: $counter,
+ ),
+)
+
+$collection = new InMemoryCollection([
+ new Node('192.168.0.1'),
+ new Node('192.168.0.2'),
+ new Node('192.168.0.3'),
+ new Node('192.168.0.4'),
+]);
+
+while (true) {
+ /** @var NodeInterface $node */
+ $node = $throttler->pick($collection);
+
+ // ...
+
+ $counter++;
}
```
-In the example above, we wrote the counter with Redis.
+You can save the current request count in any storage and resume work from the last iteration as shown below:
```php
-/** @var Predis\Client $client */
+pick($collection);
+
+ // ...
+}
/** @var string $serialized */
-$serialized = serialize($strategy);
+$serialized = serialize($throttler);
```
-The serialization result will return an instance of SmoothWeightedRoundRobinStrategy with the actual weights for the nodes:
+You can save the serialization result in any storage and restore the strategy's operation using the `unserialize(string $data, array $options = []): mixed` function. The serialization result will return an instance of `Orangesoft\Throttler\SmoothWeightedRoundRobinThrottler::class` with the actual weights for the nodes:
```php
-/** @var SmoothWeightedRoundRobinStrategy $strategy */
-$strategy = unserialize($serialized);
+/** @var SmoothWeightedRoundRobinThrottler $throttler */
+$throttler = unserialize($serialized);
+
+while (true) {
+ /** @var NodeInterface $node */
+ $node = $throttler->pick($collection);
+
+ // ...
+}
```
-This way you can preserve the order of nodes for a given strategy between PHP calls.
+This way keep state the order of nodes for a given strategy between PHP calls.
-## Dynamically change strategy
+## Custom counter
-You can dynamically change the strategy from the client code. To do this, configure the [MultipleDynamicStrategy](../src/Strategy/MultipleDynamicStrategy.php) with the strategies you need:
+To create a custom counter for *round-robin based* strategies, for example, using Redis, you need to implement the `Orangesoft\Throttler\Counter\CounterInterface::next(string $name = 'default'): int` interface as shown below:
```php
$context
+ */
+ public function pick(CollectionInterface $collection, array $context = []) : NodeInterface
+ {
+ if ($collection->isEmpty()) {
+ throw new \RuntimeException('Collection of nodes mustn\'t be empty.');
+ }
+
+ // ...
+ }
+};
```
-Pass the `strategy_name` parameter through the context with the name of the strategy class according to which the collection needs to be balanced:
+With your own strategies, you can wrap existing ones and, for example, cache their behavior.
+
+## Multiple throttler
+
+To dynamically change strategies from client code, use `Orangesoft\Throttler\MultipleThrottler::class` after pre-configuring it with preferred strategies:
```php
-/** @var Node $node */
-$node = $throttler->pick($collection, [
- 'strategy_name' => RoundRobinStrategy::class,
+pick(
+ collection: $collection,
+ context: [
+ 'throttler' => RoundRobinStrategy::class,
+ 'counter' => InMemoryCounter::class,
+ ],
+);
+```
+
+The context parameter `throttler` specifies the class of the strategy to be accessed, while `counter` sets the name for the counter, which will be passed to the `Orangesoft\Throttler\Counter\CounterInterface::next(string $name = 'default'): int` method to avoid conflicts between strategies.
## Balance cluster
-You can divide the nodes into clusters and set a specific balancing strategy for each cluster. To do this, configure the [ClusterDetermineStrategy](../src/Strategy/ClusterDetermineStrategy.php) as shown below:
+You can add specific node collections to clusters and run the load balancer only for a specific cluster. Configure `Orangesoft\Throttler\Cluster\ClusterPool::class`, where you need to bind the desired strategies to the cluster name, and create the required number of clusters `Orangesoft\Throttler\Cluster\Cluster::class`:
```php
balance(
+ pool: $pool,
+ context: [
+ 'counter' => 'Mercury',
+ ],
);
```
-Create clusters from nodes:
+Note that you can also pass an optional context parameter `counter` with the counter name to avoid conflicts between clusters that use *round-robin based* strategies.
+
+## Guzzle middleware
+
+Let's break down an example of how to configure Guzzle for proxy balancing using middleware, which allows hiding a real IP server. To install the necessary packages to demonstrate proxy balancing in Guzzle, let's use the [Composer](https://getcomposer.org/) package manager:
+
+```text
+composer require \
+ && orangesoft/throttler \
+ && guzzlehttp/guzzle \
+ && psr/http-message \
+ && predis/predis
+```
+
+The package [guzzlehttp/guzzle](https://github.com/guzzle/guzzle) is necessary for HTTP requests, [psr/http-message](https://github.com/php-fig/http-message) — HTTP message interfaces, [predis/predis](https://github.com/predis/predis) — for saving balancing strategies between the callings of PHP processes.
+
+Write proxy middleware for Guzzle that will add the proxy to every HTTP-requests according to the chosen strategy:
```php
-$collection = new Collection([
- new Node('node1'),
- new Node('node2'),
- new Node('node3'),
+ $context
+ */
+ public function __construct(
+ private ThrottlerInterface $throttler,
+ private CollectionInterface $collection,
+ private array $context = [],
+ ) {
+ }
+
+ public function __invoke(callable $handler): \Closure
+ {
+ return function (RequestInterface $request, array $options) use ($handler): ResponseInterface {
+ /** @var NodeInterface $node */
+ $node = $this->throttler->pick($this->collection, $this->context);
+
+ $options['proxy'] = $node->getName();
+
+ return $handler($request, $options);
+ };
+ }
+}
+```
+
+Create simple in-memory storage in Redis to keep load balancing counting between PHP calls for *round-robin based* strategies. Below is an example of how to implement the in-memory counter with the help of the `predis/predis` package:
+
+```php
+redis->exists($name)) {
+ $this->redis->set($name, -1);
+ }
+
+ return $this->redis->incr($name);
+ }
+}
+```
+
+Now it’s time to configure load balancer and connect proxy middleware to Guzzle:
+
+```php
+push(new ProxyMiddleware($throttler, $collection));
+$guzzle = new GuzzleClient(['handler' => $stack]);
```
-Using the `balance()` method, force your cluster to balance:
+We can use Guzzle as always:
```php
-/** @var Node $node */
-$node = $cluster->balance($throttler);
+while (true) {
+ /** @var ResponseInterface $response */
+ $response = $guzzle->get('https://httpbin.org/ip');
+
+ // ...
+}
+```
+
+The result of the proxy balancing will be as follows:
+
+```text
++---------+-----------------------+
+| request | proxy |
++---------+-----------------------+
+| 1 | user:pass@192.168.0.1 |
+| 2 | user:pass@192.168.0.1 |
+| 3 | user:pass@192.168.0.1 |
+| 4 | user:pass@192.168.0.1 |
+| 5 | user:pass@192.168.0.1 |
+| 6 | user:pass@192.168.0.2 |
+| 7 | user:pass@192.168.0.3 |
+| 8 | user:pass@192.168.0.4 |
+| n | etc. |
++---------+-----------------------+
```
-This method is well suited in cases where the nodes can be divided according to a specific criterion and each cluster needs its own balancing strategy.
+Proxy balancing in Guzzle is one of the package's use cases. You can use it for distributing requests across different microservices to ensure even utilization and prevent bottlenecks, read-only database queries across multiple database servers to improve performance, API requests across multiple backend services to ensure high availability and fault tolerance, etc.
diff --git a/helpers.php b/helpers.php
new file mode 100644
index 0000000..da32a72
--- /dev/null
+++ b/helpers.php
@@ -0,0 +1,21 @@
+
-
-
-
- iterator_to_array($this->nodes, false)
-
-
- $node
-
-
- $node
-
-
- Node[]
-
-
-
-
- $callback
-
-
-
-
- $context['cluster_name']
-
-
- $this->clusterNames[$context['cluster_name']]
- $this->clusterNames[$context['cluster_name']]
-
-
- $this->clusterNames
- $this->strategies
-
-
- StrategyInterface
-
-
-
-
- $context['strategy_name']
-
-
- StrategyInterface
-
-
-
-
- $context['counter_name'] ?? self::class
-
-
-
-
- $this->currentWeights
-
-
- getNode
-
-
-
-
- getNode
-
-
- Node
-
-
- $collection->getNode($index)
-
-
-
-
- $context['counter_name'] ?? self::class
-
-
-
-
- assertIsIterable
-
-
-
+
diff --git a/src/Cluster/Cluster.php b/src/Cluster/Cluster.php
new file mode 100644
index 0000000..4da6fcd
--- /dev/null
+++ b/src/Cluster/Cluster.php
@@ -0,0 +1,28 @@
+ $context
+ */
+ public function balance(ThrottlerInterface $pool, array $context = []): NodeInterface
+ {
+ return $pool->pick($this->collection, array_merge($context, [
+ 'cluster' => $this->name,
+ ]));
+ }
+}
diff --git a/src/Cluster/ClusterInterface.php b/src/Cluster/ClusterInterface.php
new file mode 100644
index 0000000..dd989e2
--- /dev/null
+++ b/src/Cluster/ClusterInterface.php
@@ -0,0 +1,16 @@
+ $context
+ */
+ public function balance(ThrottlerInterface $pool, array $context = []): NodeInterface;
+}
diff --git a/src/Cluster/ClusterPool.php b/src/Cluster/ClusterPool.php
new file mode 100644
index 0000000..6b98c0f
--- /dev/null
+++ b/src/Cluster/ClusterPool.php
@@ -0,0 +1,56 @@
+
+ */
+ private array $throttlers = [];
+ /**
+ * @var array
+ */
+ private array $clusterNames = [];
+
+ public function __construct(ClusterSet ...$clusterSets)
+ {
+ foreach ($clusterSets as $clusterSet) {
+ $id = $clusterSet->throttler::class;
+ $this->throttlers[$id] = $clusterSet->throttler;
+
+ foreach ($clusterSet->clusterNames as $clusterName) {
+ if (isset($this->clusterNames[$clusterName])) {
+ throw new \UnexpectedValueException(sprintf('The cluster "%s" has already been added.', $clusterName)); // @codeCoverageIgnore
+ }
+
+ $this->clusterNames[$clusterName] = $id;
+ }
+ }
+ }
+
+ public function pick(CollectionInterface $collection, array $context = []): NodeInterface
+ {
+ if (!isset($context['cluster'])) {
+ throw new \RuntimeException('Required parameter "cluster" is missing.'); // @codeCoverageIgnore
+ }
+
+ if (!\is_string($context['cluster'])) {
+ throw new \RuntimeException(sprintf('The parameter "cluster" must be as a string, %s given.', get_debug_type($context['cluster']))); // @codeCoverageIgnore
+ }
+
+ if (!isset($this->clusterNames[$context['cluster']])) {
+ throw new \RuntimeException(sprintf('The cluster "%s" is undefined.', $context['cluster'])); // @codeCoverageIgnore
+ }
+
+ $throttler = $this->throttlers[$this->clusterNames[$context['cluster']]];
+
+ return $throttler->pick($collection, $context);
+ }
+}
diff --git a/src/Cluster/ClusterSet.php b/src/Cluster/ClusterSet.php
new file mode 100644
index 0000000..2873d8a
--- /dev/null
+++ b/src/Cluster/ClusterSet.php
@@ -0,0 +1,22 @@
+weight - $b->weight;
- }
-}
diff --git a/src/Collection/Cluster.php b/src/Collection/Cluster.php
deleted file mode 100644
index b4497fd..0000000
--- a/src/Collection/Cluster.php
+++ /dev/null
@@ -1,23 +0,0 @@
-pick($this->collection, array_merge($context, [
- 'cluster_name' => $this->name,
- ]));
- }
-}
diff --git a/src/Collection/ClusterInterface.php b/src/Collection/ClusterInterface.php
deleted file mode 100644
index 1cd6a41..0000000
--- a/src/Collection/ClusterInterface.php
+++ /dev/null
@@ -1,12 +0,0 @@
-nodes = new \SplObjectStorage();
-
- foreach ($nodes as $node) {
- $this->addNode($node);
- }
- }
-
- public function addNode(Node $node): self
- {
- if (0 >= $node->weight) {
- $this->isWeighted = false;
- }
-
- $this->nodes->attach($node);
-
- return $this;
- }
-
- public function getNode(int $index): Node
- {
- if ($index > $this->nodes->count()) {
- throw new \InvalidArgumentException(sprintf('Cannot find node at index "%d".', $index));
- }
-
- $this->nodes->rewind();
-
- while ($index--) {
- $this->nodes->next();
- }
-
- /** @var Node $node */
- $node = $this->nodes->current();
-
- return $node;
- }
-
- public function hasNode(Node $node): bool
- {
- return $this->nodes->contains($node);
- }
-
- public function removeNode(Node $node): void
- {
- $this->nodes->detach($node);
- }
-
- public function purge(): void
- {
- $this->nodes->removeAll($this->nodes);
- }
-
- public function isWeighted(): bool
- {
- return $this->isWeighted;
- }
-
- public function isEmpty(): bool
- {
- return 0 === $this->nodes->count();
- }
-
- public function count(): int
- {
- return $this->nodes->count();
- }
-
- /**
- * @return Node[]
- */
- public function toArray(): array
- {
- return iterator_to_array($this->nodes, false);
- }
-
- public function getIterator(): \Traversable
- {
- return $this->nodes;
- }
-}
diff --git a/src/Collection/CollectionInterface.php b/src/Collection/CollectionInterface.php
index aaec000..095f40f 100644
--- a/src/Collection/CollectionInterface.php
+++ b/src/Collection/CollectionInterface.php
@@ -4,24 +4,29 @@
namespace Orangesoft\Throttler\Collection;
+/**
+ * @template T of NodeInterface
+ * @template-extends \IteratorAggregate
+ */
interface CollectionInterface extends \Countable, \IteratorAggregate
{
- public function addNode(Node $node): self;
+ public function add(NodeInterface $node): self;
- public function getNode(int $index): Node;
+ public function get(int $key): NodeInterface;
- public function hasNode(Node $node): bool;
+ public function has(NodeInterface $node): bool;
- public function removeNode(Node $node): void;
+ public function remove(NodeInterface $node): self;
- public function purge(): void;
-
- public function isWeighted(): bool;
+ /**
+ * @param callable(T, T): int $callback
+ */
+ public function sort(callable $callback): self;
public function isEmpty(): bool;
/**
- * @return Node[]
+ * @return array
*/
public function toArray(): array;
}
diff --git a/src/Collection/Desc.php b/src/Collection/Desc.php
deleted file mode 100644
index 7fb7083..0000000
--- a/src/Collection/Desc.php
+++ /dev/null
@@ -1,13 +0,0 @@
-weight - $a->weight;
- }
-}
diff --git a/src/Collection/Exception/EmptyCollectionException.php b/src/Collection/Exception/EmptyCollectionException.php
deleted file mode 100644
index 95c9df3..0000000
--- a/src/Collection/Exception/EmptyCollectionException.php
+++ /dev/null
@@ -1,13 +0,0 @@
-
+ */
+ private array $nodes;
+ /**
+ * @var array
+ */
+ private array $keys;
+
+ /**
+ * @param NodeInterface[] $nodes
+ */
+ public function __construct(array $nodes = [])
+ {
+ $this->nodes = [];
+ $this->keys = [];
+
+ foreach ($nodes as $node) {
+ if ($this->has($node)) {
+ throw new \InvalidArgumentException(sprintf('All nodes must be unique, "%s" given as duplicate.', $node->getName()));
+ }
+
+ $this->nodes[] = $node;
+ $this->keys[$node->getName()] = array_key_last($this->nodes);
+ }
+ }
+
+ public function add(NodeInterface $node): self
+ {
+ if ($this->has($node)) {
+ throw new \UnexpectedValueException(sprintf('The node "%s" has been already added.', $node->getName()));
+ }
+
+ $self = clone $this;
+ $self->nodes[] = $node;
+ $self->keys[$node->getName()] = array_key_last($self->nodes);
+
+ return $self;
+ }
+
+ public function get(int $key): NodeInterface
+ {
+ if (!isset($this->nodes[$key])) {
+ throw new \OutOfRangeException(sprintf('Can\'t get node at key "%d".', $key));
+ }
+
+ return $this->nodes[$key];
+ }
+
+ public function has(NodeInterface $node): bool
+ {
+ return \array_key_exists($node->getName(), $this->keys);
+ }
+
+ public function remove(NodeInterface $node): self
+ {
+ if (!$this->has($node)) {
+ throw new \UnexpectedValueException(sprintf('The node "%s" hasn\'t been already added.', $node->getName()));
+ }
+
+ $self = clone $this;
+ unset($self->nodes[$self->keys[$node->getName()]]);
+
+ return new self($self->toArray());
+ }
+
+ /**
+ * @param callable(T, T): int $callback
+ */
+ public function sort(callable $callback): self
+ {
+ $nodes = $this->toArray();
+
+ /** @psalm-suppress InvalidArgument */
+ usort($nodes, $callback);
+
+ return new self($nodes);
+ }
+
+ public function isEmpty(): bool
+ {
+ return 0 === $this->count();
+ }
+
+ public function count(): int
+ {
+ return \count($this->nodes);
+ }
+
+ /**
+ * @return array
+ */
+ public function toArray(): array
+ {
+ return $this->nodes;
+ }
+
+ /**
+ * @psalm-suppress ImplementedReturnTypeMismatch
+ * @return \ArrayIterator
+ */
+ public function getIterator(): \ArrayIterator
+ {
+ return new \ArrayIterator($this->nodes);
+ }
+}
diff --git a/src/Collection/Node.php b/src/Collection/Node.php
index 96dccd0..52d2d82 100644
--- a/src/Collection/Node.php
+++ b/src/Collection/Node.php
@@ -4,12 +4,33 @@
namespace Orangesoft\Throttler\Collection;
-final class Node
+final class Node implements NodeInterface
{
+ /**
+ * @param array $payload
+ */
public function __construct(
- public readonly string $name,
- public readonly int $weight = 0,
- public readonly array $info = [],
+ private string $name,
+ private int $weight = 0,
+ private array $payload = [],
) {
}
+
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ public function getWeight(): int
+ {
+ return $this->weight;
+ }
+
+ /**
+ * @return array
+ */
+ public function getPayload(): array
+ {
+ return $this->payload;
+ }
}
diff --git a/src/Collection/NodeInterface.php b/src/Collection/NodeInterface.php
new file mode 100644
index 0000000..682956c
--- /dev/null
+++ b/src/Collection/NodeInterface.php
@@ -0,0 +1,17 @@
+
+ */
+ public function getPayload(): array;
+}
diff --git a/src/Collection/Sort/Asc.php b/src/Collection/Sort/Asc.php
new file mode 100644
index 0000000..431032d
--- /dev/null
+++ b/src/Collection/Sort/Asc.php
@@ -0,0 +1,15 @@
+getWeight() - $b->getWeight();
+ }
+}
diff --git a/src/Collection/Sort/Desc.php b/src/Collection/Sort/Desc.php
new file mode 100644
index 0000000..b00f40f
--- /dev/null
+++ b/src/Collection/Sort/Desc.php
@@ -0,0 +1,15 @@
+getWeight() - $a->getWeight();
+ }
+}
diff --git a/src/Collection/Sorter.php b/src/Collection/Sorter.php
deleted file mode 100644
index 87e73a8..0000000
--- a/src/Collection/Sorter.php
+++ /dev/null
@@ -1,21 +0,0 @@
-toArray();
-
- usort($nodes, $callback);
-
- $collection->purge();
-
- foreach ($nodes as $node) {
- $collection->addNode($node);
- }
- }
-}
diff --git a/src/Strategy/CounterInterface.php b/src/Counter/CounterInterface.php
similarity index 74%
rename from src/Strategy/CounterInterface.php
rename to src/Counter/CounterInterface.php
index f27c6f1..2b85d40 100644
--- a/src/Strategy/CounterInterface.php
+++ b/src/Counter/CounterInterface.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace Orangesoft\Throttler\Strategy;
+namespace Orangesoft\Throttler\Counter;
interface CounterInterface
{
diff --git a/src/Strategy/InMemoryCounter.php b/src/Counter/InMemoryCounter.php
similarity index 76%
rename from src/Strategy/InMemoryCounter.php
rename to src/Counter/InMemoryCounter.php
index 3cd9576..2ebdcdc 100644
--- a/src/Strategy/InMemoryCounter.php
+++ b/src/Counter/InMemoryCounter.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace Orangesoft\Throttler\Strategy;
+namespace Orangesoft\Throttler\Counter;
final class InMemoryCounter implements CounterInterface
{
@@ -22,6 +22,9 @@ public function next(string $name = 'default'): int
$this->counter[$name] = $this->start;
}
- return $this->counter[$name]++;
+ $next = $this->counter[$name];
+ ++$this->counter[$name];
+
+ return $next;
}
}
diff --git a/src/FrequencyRandomThrottler.php b/src/FrequencyRandomThrottler.php
new file mode 100644
index 0000000..c98185a
--- /dev/null
+++ b/src/FrequencyRandomThrottler.php
@@ -0,0 +1,37 @@
+ $context
+ */
+ public function pick(CollectionInterface $collection, array $context = []): NodeInterface
+ {
+ if ($collection->isEmpty()) {
+ throw new \RuntimeException('Collection of nodes mustn\'t be empty.'); // @codeCoverageIgnore
+ }
+
+ $sorted = $collection->sort(new Desc());
+ $total = \count($sorted);
+ $lowerKey = (int) ceil($this->threshold * $total);
+ $higherKey = $lowerKey + (1 < $total ? 1 : 0);
+ $probability = mt_rand() / mt_getrandmax();
+ $key = $this->frequency >= $probability ? mt_rand(1, $lowerKey) : mt_rand($higherKey, $total);
+
+ return $sorted->get($key - 1);
+ }
+}
diff --git a/src/MultipleThrottler.php b/src/MultipleThrottler.php
new file mode 100644
index 0000000..28bf7f8
--- /dev/null
+++ b/src/MultipleThrottler.php
@@ -0,0 +1,45 @@
+
+ */
+ private array $throttlers = [];
+
+ public function __construct(ThrottlerInterface ...$throttlers)
+ {
+ foreach ($throttlers as $throttler) {
+ $this->throttlers[$throttler::class] = $throttler;
+ }
+ }
+
+ /**
+ * @param array $context
+ */
+ public function pick(CollectionInterface $collection, array $context = []): NodeInterface
+ {
+ if (!isset($context['throttler']) || !\is_string($context['throttler'])) {
+ throw new \RuntimeException('The required parameter "throttler" must be passed as a throttler\'s class name.'); // @codeCoverageIgnore
+ }
+
+ if (!isset($this->throttlers[$context['throttler']])) {
+ throw new \RuntimeException(sprintf('The throttler "%s" is undefined.', $context['throttler'])); // @codeCoverageIgnore
+ }
+
+ if (!class_exists($context['throttler']) || !is_a($context['throttler'], ThrottlerInterface::class, true)) {
+ throw new \UnexpectedValueException(sprintf('The throttler must be a class that exists and implements "%s" interface, "%s" given.', ThrottlerInterface::class, get_debug_type($context['throttler']))); // @codeCoverageIgnore
+ }
+
+ $throttler = $this->throttlers[$context['throttler']];
+
+ return $throttler->pick($collection, $context);
+ }
+}
diff --git a/src/RandomThrottler.php b/src/RandomThrottler.php
new file mode 100644
index 0000000..5c1bc49
--- /dev/null
+++ b/src/RandomThrottler.php
@@ -0,0 +1,25 @@
+ $context
+ */
+ public function pick(CollectionInterface $collection, array $context = []): NodeInterface
+ {
+ if ($collection->isEmpty()) {
+ throw new \RuntimeException('Collection of nodes mustn\'t be empty.'); // @codeCoverageIgnore
+ }
+
+ $key = mt_rand(0, \count($collection) - 1);
+
+ return $collection->get($key);
+ }
+}
diff --git a/src/RoundRobinThrottler.php b/src/RoundRobinThrottler.php
new file mode 100644
index 0000000..aa1e140
--- /dev/null
+++ b/src/RoundRobinThrottler.php
@@ -0,0 +1,36 @@
+ $context
+ */
+ public function pick(CollectionInterface $collection, array $context = []): NodeInterface
+ {
+ if ($collection->isEmpty()) {
+ throw new \RuntimeException('Collection of nodes mustn\'t be empty.'); // @codeCoverageIgnore
+ }
+
+ if (isset($context['counter']) && !\is_string($context['counter'])) {
+ throw new \RuntimeException(sprintf('The parameter "counter" must be as a string, %s given.', get_debug_type($context['counter']))); // @codeCoverageIgnore
+ }
+
+ $counter = $context['counter'] ?? spl_object_hash($collection);
+ $key = $this->counter->next($counter) % \count($collection);
+
+ return $collection->get($key);
+ }
+}
diff --git a/src/SmoothWeightedRoundRobinThrottler.php b/src/SmoothWeightedRoundRobinThrottler.php
new file mode 100644
index 0000000..1c0d893
--- /dev/null
+++ b/src/SmoothWeightedRoundRobinThrottler.php
@@ -0,0 +1,70 @@
+>
+ */
+ private array $weights = [];
+ /**
+ * @var array>
+ */
+ private array $currentWeights = [];
+
+ /**
+ * @param array $context
+ */
+ public function pick(CollectionInterface $collection, array $context = []): NodeInterface
+ {
+ if ($collection->isEmpty()) {
+ throw new \RuntimeException('Collection of nodes mustn\'t be empty.'); // @codeCoverageIgnore
+ }
+
+ if (isset($context['counter']) && !\is_string($context['counter'])) {
+ throw new \RuntimeException(sprintf('The parameter "counter" must be as a string, %s given.', get_debug_type($context['counter']))); // @codeCoverageIgnore
+ }
+
+ $counter = $context['counter'] ?? spl_object_hash($collection);
+
+ if (!isset($this->weights[$counter]) || !isset($this->currentWeights[$counter])) {
+ $this->weights[$counter] = [];
+ $this->currentWeights[$counter] = [];
+ }
+
+ if (0 === \count($this->weights[$counter]) || 0 === \count($this->currentWeights[$counter])) {
+ foreach ($collection as $key => $node) {
+ if (0 === $node->getWeight()) {
+ throw new \RuntimeException('All nodes in the collection must be weighted.'); // @codeCoverageIgnore
+ }
+
+ $this->weights[$counter][$key] = $node->getWeight();
+ $this->currentWeights[$counter][$key] = $node->getWeight();
+ }
+ }
+
+ if (0 === \count($this->currentWeights[$counter])) {
+ throw new \RuntimeException('Current weights are empty.'); // @codeCoverageIgnore
+ }
+
+ $maxCurrentWeight = max($this->currentWeights[$counter]);
+
+ if (false === $maxCurrentWeightKey = array_search($maxCurrentWeight, $this->currentWeights[$counter], true)) {
+ throw new \LogicException('Couldn\'t find max current weight index.'); // @codeCoverageIgnore
+ }
+
+ $this->currentWeights[$counter][$maxCurrentWeightKey] -= array_sum($this->weights[$counter]);
+
+ foreach ($this->weights[$counter] as $key => $weight) {
+ $this->currentWeights[$counter][$key] += $weight;
+ }
+
+ return $collection->get($maxCurrentWeightKey);
+ }
+}
diff --git a/src/Strategy/ClusterDetermineStrategy.php b/src/Strategy/ClusterDetermineStrategy.php
deleted file mode 100644
index b902779..0000000
--- a/src/Strategy/ClusterDetermineStrategy.php
+++ /dev/null
@@ -1,51 +0,0 @@
-
- */
- private array $strategies = [];
- /**
- * @var array
- */
- private array $clusterNames = [];
-
- public function __construct(ClusterSet ...$clusterSets)
- {
- foreach ($clusterSets as $key => $clusterSet) {
- $this->strategies[$key] = $clusterSet->strategy;
-
- foreach ($clusterSet->clusterNames as $clusterName) {
- if (isset($this->clusterNames[$clusterName])) {
- throw new \InvalidArgumentException(sprintf('Cluster "%s" has already been added.', $clusterName));
- }
-
- $this->clusterNames[$clusterName] = $key;
- }
- }
- }
-
- public function getNode(CollectionInterface $collection, array $context = []): Node
- {
- if (!isset($context['cluster_name'])) {
- throw new \LogicException('Required parameter "cluster_name" is missing.');
- }
-
- if (!isset($this->clusterNames[$context['cluster_name']])) {
- throw new \LogicException(sprintf('Cluster name "%s" is undefined.', $context['cluster_name']));
- }
-
- /** @var StrategyInterface $strategy */
- $strategy = $this->strategies[$this->clusterNames[$context['cluster_name']]];
-
- return $strategy->getNode($collection, $context);
- }
-}
diff --git a/src/Strategy/ClusterSet.php b/src/Strategy/ClusterSet.php
deleted file mode 100644
index ca6b7b2..0000000
--- a/src/Strategy/ClusterSet.php
+++ /dev/null
@@ -1,20 +0,0 @@
-isEmpty()) {
- throw new EmptyCollectionException();
- }
-
- $total = \count($collection);
- $low = (int) ceil($this->depth * $total);
- $high = $low + ((1 < $total) ? 1 : 0);
-
- $index = $this->isChance($this->frequency) ? mt_rand(1, $low) : mt_rand($high, $total);
-
- return $collection->getNode($index - 1);
- }
-
- private function isChance(float $frequency): bool
- {
- return $frequency * 100 >= mt_rand(1, 100);
- }
-}
diff --git a/src/Strategy/GcdCalculator.php b/src/Strategy/GcdCalculator.php
deleted file mode 100644
index b17ab27..0000000
--- a/src/Strategy/GcdCalculator.php
+++ /dev/null
@@ -1,19 +0,0 @@
-
- */
- private array $pool = [];
-
- public function __construct(StrategyInterface ...$strategies)
- {
- foreach ($strategies as $strategy) {
- $this->pool[$strategy::class] = $strategy;
- }
- }
-
- public function getNode(CollectionInterface $collection, array $context = []): Node
- {
- if (!isset($context['strategy_name'])) {
- throw new \LogicException('Required parameter "strategy_name" is missing.');
- }
-
- if (!class_exists($context['strategy_name']) || !is_a($context['strategy_name'], StrategyInterface::class, true)) {
- throw new \LogicException(sprintf('Strategy must be a class that exists and implements "%s" interface, "%s" given.', StrategyInterface::class, get_debug_type($context['strategy_name'])));
- }
-
- if (!isset($this->pool[$context['strategy_name']])) {
- throw new \LogicException(sprintf('Strategy "%s" is undefined.', $context['strategy_name']));
- }
-
- /** @var StrategyInterface $strategy */
- $strategy = $this->pool[$context['strategy_name']];
-
- return $strategy->getNode($collection, $context);
- }
-}
diff --git a/src/Strategy/RandomStrategy.php b/src/Strategy/RandomStrategy.php
deleted file mode 100644
index ff47033..0000000
--- a/src/Strategy/RandomStrategy.php
+++ /dev/null
@@ -1,23 +0,0 @@
-isEmpty()) {
- throw new EmptyCollectionException();
- }
-
- $index = mt_rand(0, \count($collection) - 1);
-
- return $collection->getNode($index);
- }
-}
diff --git a/src/Strategy/RoundRobinStrategy.php b/src/Strategy/RoundRobinStrategy.php
deleted file mode 100644
index f7ec867..0000000
--- a/src/Strategy/RoundRobinStrategy.php
+++ /dev/null
@@ -1,28 +0,0 @@
-isEmpty()) {
- throw new EmptyCollectionException();
- }
-
- $index = $this->counter->next($context['counter_name'] ?? self::class) % \count($collection);
-
- return $collection->getNode($index);
- }
-}
diff --git a/src/Strategy/SmoothWeightedRoundRobinStrategy.php b/src/Strategy/SmoothWeightedRoundRobinStrategy.php
deleted file mode 100644
index c31446e..0000000
--- a/src/Strategy/SmoothWeightedRoundRobinStrategy.php
+++ /dev/null
@@ -1,68 +0,0 @@
-
- */
- private array $weights = [];
- /**
- * @var array
- */
- private array $currentWeights = [];
-
- public function getNode(CollectionInterface $collection, array $context = []): Node
- {
- if ($collection->isEmpty()) {
- throw new EmptyCollectionException();
- }
-
- if (!$collection->isWeighted()) {
- throw new UnweightedCollectionException();
- }
-
- if (0 === \count($this->weights) || 0 === \count($this->currentWeights)) {
- /** @var array $collection */
- foreach ($collection as $index => $node) {
- $this->weights[$index] = $this->currentWeights[$index] = $node->weight;
- }
- }
-
- $maxCurrentWeightIndex = $this->getMaxCurrentWeightIndex();
-
- $this->recalculateCurrentWeights($maxCurrentWeightIndex);
-
- return $collection->getNode($maxCurrentWeightIndex);
- }
-
- private function getMaxCurrentWeightIndex(): int
- {
- $maxCurrentWeight = max($this->currentWeights);
-
- if (false === $index = array_search($maxCurrentWeight, $this->currentWeights, true)) {
- throw new \LogicException('Cannot find max current weight index.');
- }
-
- return $index;
- }
-
- private function recalculateCurrentWeights(int $maxCurrentWeightIndex): void
- {
- $recalculatedCurrentWeight = $this->currentWeights[$maxCurrentWeightIndex] - array_sum($this->weights);
-
- $this->currentWeights[$maxCurrentWeightIndex] = $recalculatedCurrentWeight;
-
- foreach ($this->weights as $index => $weight) {
- $this->currentWeights[$index] += $weight;
- }
- }
-}
diff --git a/src/Strategy/StrategyInterface.php b/src/Strategy/StrategyInterface.php
deleted file mode 100644
index 1b11174..0000000
--- a/src/Strategy/StrategyInterface.php
+++ /dev/null
@@ -1,13 +0,0 @@
-isEmpty()) {
- throw new EmptyCollectionException();
- }
-
- if (!$collection->isWeighted()) {
- throw new UnweightedCollectionException();
- }
-
- $currentWeight = 0;
-
- if (0 === $this->sumWeight) {
- $this->sumWeight = $this->calculateSumWeight($collection);
- }
-
- $randomWeight = mt_rand(1, $this->sumWeight);
-
- /** @var array $collection */
- foreach ($collection as $index => $node) {
- $currentWeight += $node->weight;
-
- if ($randomWeight <= $currentWeight) {
- return $collection->getNode($index);
- }
- }
-
- throw new \RuntimeException('You never will catch this exception.');
- }
-
- private function calculateSumWeight(CollectionInterface $collection): int
- {
- $sumWeight = 0;
-
- /** @var Node $node */
- foreach ($collection as $node) {
- $sumWeight += $node->weight;
- }
-
- return $sumWeight;
- }
-}
diff --git a/src/Strategy/WeightedRoundRobinStrategy.php b/src/Strategy/WeightedRoundRobinStrategy.php
deleted file mode 100644
index 60ebfb7..0000000
--- a/src/Strategy/WeightedRoundRobinStrategy.php
+++ /dev/null
@@ -1,85 +0,0 @@
-isEmpty()) {
- throw new EmptyCollectionException();
- }
-
- if (!$collection->isWeighted()) {
- throw new UnweightedCollectionException();
- }
-
- if (0 === $this->gcd) {
- $this->gcd = $this->calculateGcd($collection);
- }
-
- if (0 === $this->maxWeight) {
- $this->maxWeight = $this->calculateMaxWeight($collection);
- }
-
- while (true) {
- $index = $this->counter->next($context['counter_name'] ?? self::class) % \count($collection);
-
- if (0 === $index) {
- $this->currentWeight -= $this->gcd;
-
- if (0 >= $this->currentWeight) {
- $this->currentWeight = $this->maxWeight;
- }
- }
-
- $node = $collection->getNode($index);
-
- if ($node->weight >= $this->currentWeight) {
- return $node;
- }
- }
- }
-
- private function calculateGcd(CollectionInterface $collection): int
- {
- $gcd = 0;
-
- /** @var Node $node */
- foreach ($collection as $node) {
- $gcd = GcdCalculator::calculate($gcd, $node->weight);
- }
-
- return $gcd;
- }
-
- private function calculateMaxWeight(CollectionInterface $collection): int
- {
- $maxWeight = 0;
-
- /** @var Node $node */
- foreach ($collection as $node) {
- if ($node->weight >= $maxWeight) {
- $maxWeight = $node->weight;
- }
- }
-
- return $maxWeight;
- }
-}
diff --git a/src/Throttler.php b/src/Throttler.php
deleted file mode 100644
index c3430c3..0000000
--- a/src/Throttler.php
+++ /dev/null
@@ -1,22 +0,0 @@
-strategy->getNode($collection, $context);
- }
-}
diff --git a/src/ThrottlerInterface.php b/src/ThrottlerInterface.php
index 4e81068..4effacc 100644
--- a/src/ThrottlerInterface.php
+++ b/src/ThrottlerInterface.php
@@ -5,9 +5,12 @@
namespace Orangesoft\Throttler;
use Orangesoft\Throttler\Collection\CollectionInterface;
-use Orangesoft\Throttler\Collection\Node;
+use Orangesoft\Throttler\Collection\NodeInterface;
interface ThrottlerInterface
{
- public function pick(CollectionInterface $collection, array $context = []): Node;
+ /**
+ * @param array $context
+ */
+ public function pick(CollectionInterface $collection, array $context = []): NodeInterface;
}
diff --git a/src/WeightedRandomThrottler.php b/src/WeightedRandomThrottler.php
new file mode 100644
index 0000000..50ffdf7
--- /dev/null
+++ b/src/WeightedRandomThrottler.php
@@ -0,0 +1,39 @@
+ $context
+ */
+ public function pick(CollectionInterface $collection, array $context = []): NodeInterface
+ {
+ if ($collection->isEmpty()) {
+ throw new \RuntimeException('Collection of nodes mustn\'t be empty.'); // @codeCoverageIgnore
+ }
+
+ $currentWeight = 0;
+ $sumWeight = array_sum(array_map(static fn (NodeInterface $node): int => $node->getWeight(), $collection->toArray()));
+ $randomWeight = mt_rand(1, $sumWeight);
+
+ foreach ($collection as $node) {
+ if (0 === $node->getWeight()) {
+ throw new \RuntimeException('All nodes in the collection must be weighted.'); // @codeCoverageIgnore
+ }
+
+ $currentWeight += $node->getWeight();
+
+ if ($randomWeight <= $currentWeight) {
+ return $node;
+ }
+ }
+
+ throw new \RuntimeException('You never will catch this exception.'); // @codeCoverageIgnore
+ }
+}
diff --git a/src/WeightedRoundRobinThrottler.php b/src/WeightedRoundRobinThrottler.php
new file mode 100644
index 0000000..80a5465
--- /dev/null
+++ b/src/WeightedRoundRobinThrottler.php
@@ -0,0 +1,81 @@
+
+ */
+ private array $gcdWeight = [];
+ /**
+ * @var array
+ */
+ private array $maxWeight = [];
+ /**
+ * @var array
+ */
+ private array $currentWeight = [];
+
+ public function __construct(
+ private CounterInterface $counter,
+ ) {
+ }
+
+ /**
+ * @param array $context
+ */
+ public function pick(CollectionInterface $collection, array $context = []): NodeInterface
+ {
+ if ($collection->isEmpty()) {
+ throw new \RuntimeException('Collection of nodes mustn\'t be empty.'); // @codeCoverageIgnore
+ }
+
+ if (isset($context['counter']) && !\is_string($context['counter'])) {
+ throw new \RuntimeException(sprintf('The parameter "counter" must be as a string, %s given.', get_debug_type($context['counter']))); // @codeCoverageIgnore
+ }
+
+ $counter = $context['counter'] ?? spl_object_hash($collection);
+
+ if (!isset($this->gcdWeight[$counter]) || !isset($this->maxWeight[$counter]) || !isset($this->currentWeight[$counter])) {
+ $this->gcdWeight[$counter] = 0;
+ $this->maxWeight[$counter] = 0;
+ $this->currentWeight[$counter] = 0;
+ }
+
+ if (0 === $this->gcdWeight[$counter] || 0 === $this->maxWeight[$counter] || 0 === $this->currentWeight[$counter]) {
+ foreach ($collection as $node) {
+ if (0 === $node->getWeight()) {
+ throw new \RuntimeException('All nodes in the collection must be weighted.'); // @codeCoverageIgnore
+ }
+
+ $this->gcdWeight[$counter] = gcd($this->gcdWeight[$counter], $node->getWeight());
+ $this->maxWeight[$counter] = max($this->maxWeight[$counter], $node->getWeight());
+ }
+ }
+
+ while (true) {
+ $key = $this->counter->next($counter) % \count($collection);
+
+ if (0 == $key) {
+ $this->currentWeight[$counter] -= $this->gcdWeight[$counter];
+
+ if (0 >= $this->currentWeight[$counter]) {
+ $this->currentWeight[$counter] = $this->maxWeight[$counter];
+ }
+ }
+
+ $node = $collection->get($key);
+
+ if ($node->getWeight() >= $this->currentWeight[$counter]) {
+ return $node;
+ }
+ }
+ }
+}
diff --git a/tests/Cluster/ClusterTest.php b/tests/Cluster/ClusterTest.php
new file mode 100644
index 0000000..707e4e3
--- /dev/null
+++ b/tests/Cluster/ClusterTest.php
@@ -0,0 +1,49 @@
+balance($pool);
+ }
+
+ $this->assertSame($expectedNodes, array_map(static fn (NodeInterface $actualNode): string => $actualNode->getName(), $actualNodes));
+ }
+}
diff --git a/tests/Collection/ClusterTest.php b/tests/Collection/ClusterTest.php
deleted file mode 100644
index 4c65ecf..0000000
--- a/tests/Collection/ClusterTest.php
+++ /dev/null
@@ -1,35 +0,0 @@
-assertSame($node, $cluster->balance($throttler));
- }
-}
diff --git a/tests/Collection/CollectionTest.php b/tests/Collection/CollectionTest.php
deleted file mode 100644
index 0b26efb..0000000
--- a/tests/Collection/CollectionTest.php
+++ /dev/null
@@ -1,171 +0,0 @@
-assertCount(4, $collection);
- }
-
- public function testGetNode(): void
- {
- $nodes = [
- new Node('node1'),
- new Node('node2'),
- new Node('node3'),
- ];
-
- $collection = new Collection($nodes);
-
- $this->assertSame($nodes[2], $collection->getNode(2));
- }
-
- public function testHasNode(): void
- {
- $node = new Node('node1');
-
- $collection = new Collection([
- $node,
- ]);
-
- $this->assertTrue($collection->hasNode($node));
- }
-
- public function testRemoveNode(): void
- {
- $node = new Node('node1');
-
- $collection = new Collection([
- $node,
- ]);
-
- $collection->removeNode($node);
-
- $this->assertFalse($collection->hasNode($node));
- }
-
- public function testReindex(): void
- {
- $nodes = [
- new Node('node1'),
- new Node('node2'),
- new Node('node3'),
- ];
-
- $collection = new Collection($nodes);
-
- $collection->removeNode($nodes[0]);
-
- $this->assertSame($nodes[1], $collection->getNode(0));
- }
-
- public function testPurge(): void
- {
- $collection = new Collection([
- new Node('node1'),
- new Node('node2'),
- new Node('node3'),
- ]);
-
- $collection->purge();
-
- $this->assertCount(0, $collection);
- }
-
- public function testWeightedCollection(): void
- {
- $collection = new Collection([
- new Node('node1', 5),
- new Node('node2', 1),
- new Node('node3', 1),
- ]);
-
- $this->assertTrue($collection->isWeighted());
- }
-
- public function testUnweightedCollection(): void
- {
- $collection = new Collection([
- new Node('node1', 5),
- new Node('node2', 0),
- new Node('node3', 0),
- ]);
-
- $this->assertFalse($collection->isWeighted());
- }
-
- public function testEmpty(): void
- {
- $collection = new Collection();
-
- $this->assertTrue($collection->isEmpty());
- }
-
- public function testNotEmpty(): void
- {
- $collection = new Collection([
- new Node('node1'),
- new Node('node2'),
- new Node('node3'),
- ]);
-
- $this->assertFalse($collection->isEmpty());
- }
-
- public function testToArray(): void
- {
- $collection = new Collection([
- new Node('node1'),
- new Node('node2'),
- new Node('node3'),
- ]);
-
- $this->assertIsArray($collection->toArray());
- $this->assertCount(3, $collection->toArray());
- }
-
- public function testCountable(): void
- {
- $collection = new Collection([
- new Node('node1'),
- new Node('node2'),
- new Node('node3'),
- ]);
-
- $this->assertCount(3, $collection);
- }
-
- public function testIterable(): void
- {
- $expectedNodes = [
- new Node('node1'),
- new Node('node2'),
- new Node('node3'),
- ];
-
- $collection = new Collection($expectedNodes);
-
- $this->assertIsIterable($collection);
-
- $actualNodes = iterator_to_array($collection);
-
- $this->assertSame($expectedNodes[0], $actualNodes[0]);
- $this->assertSame($expectedNodes[1], $actualNodes[1]);
- $this->assertSame($expectedNodes[2], $actualNodes[2]);
- }
-}
diff --git a/tests/Collection/InMemoryCollectionTest.php b/tests/Collection/InMemoryCollectionTest.php
new file mode 100644
index 0000000..6dfe711
--- /dev/null
+++ b/tests/Collection/InMemoryCollectionTest.php
@@ -0,0 +1,223 @@
+assertSame($expectedResult, array_map(static fn (NodeInterface $node): string => $node->getName(), $collection->toArray()));
+ }
+
+ public function testCreateCollectionWithNotUniqueNodes(): void
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('All nodes must be unique, "192.168.0.1" given as duplicate.');
+
+ new InMemoryCollection([
+ new Node('192.168.0.1'),
+ new Node('192.168.0.1'),
+ ]);
+ }
+
+ public function testAddNodeToExistedCollection(): void
+ {
+ $collection = new InMemoryCollection([
+ new Node('192.168.0.1'),
+ new Node('192.168.0.2'),
+ ]);
+
+ $other = $collection->add(new Node('192.168.0.3'));
+
+ $expectedResult = [
+ '192.168.0.1',
+ '192.168.0.2',
+ '192.168.0.3',
+ ];
+
+ $this->assertNotSame($collection, $other);
+ $this->assertSame($expectedResult, array_map(static fn (NodeInterface $node): string => $node->getName(), $other->toArray()));
+ }
+
+ public function testAddTheSameNodeToExistedCollection(): void
+ {
+ $collection = new InMemoryCollection([
+ new Node('192.168.0.1'),
+ new Node('192.168.0.2'),
+ ]);
+
+ $this->expectException(\UnexpectedValueException::class);
+ $this->expectExceptionMessage('The node "192.168.0.2" has been already added.');
+
+ $collection->add(new Node('192.168.0.2'));
+ }
+
+ public function testGetNodeByKey(): void
+ {
+ $collection = new InMemoryCollection([
+ new Node('192.168.0.1'),
+ new Node('192.168.0.2'),
+ new Node('192.168.0.3'),
+ ]);
+
+ $expectedResult = [
+ '192.168.0.1',
+ '192.168.0.2',
+ '192.168.0.3',
+ ];
+ $actualResult = [];
+
+ for ($i = 0; $i < 3; ++$i) {
+ $actualResult[] = $collection->get($i);
+ }
+
+ $this->assertSame($expectedResult, array_map(static fn (NodeInterface $node): string => $node->getName(), $actualResult));
+ }
+
+ public function testGetOutOfRangeNodeByKey(): void
+ {
+ $collection = new InMemoryCollection([
+ new Node('192.168.0.1'),
+ new Node('192.168.0.2'),
+ new Node('192.168.0.3'),
+ ]);
+
+ $this->expectException(\OutOfRangeException::class);
+ $this->expectExceptionMessage('Can\'t get node at key "3".');
+
+ $collection->get(3);
+ }
+
+ public function testHasNodeInCollection(): void
+ {
+ $collection = new InMemoryCollection([
+ new Node('192.168.0.1'),
+ new Node('192.168.0.2'),
+ new Node('192.168.0.3'),
+ ]);
+
+ $this->assertTrue($collection->has(new Node('192.168.0.1')));
+ }
+
+ public function testHasNodeInEmptyCollection(): void
+ {
+ $collection = new InMemoryCollection();
+
+ $this->assertFalse($collection->has(new Node('192.168.0.1')));
+ }
+
+ public function testRemoveNode(): void
+ {
+ $collection = new InMemoryCollection([
+ new Node('192.168.0.1'),
+ new Node('192.168.0.2'),
+ new Node('192.168.0.3'),
+ ]);
+
+ $other = $collection->remove(new Node('192.168.0.3'));
+
+ $expectedResult = [
+ '192.168.0.1',
+ '192.168.0.2',
+ ];
+
+ $this->assertNotSame($collection, $other);
+ $this->assertSame($expectedResult, array_map(static fn (NodeInterface $node): string => $node->getName(), $other->toArray()));
+ }
+
+ public function testRemoveNotAddedNode(): void
+ {
+ $collection = new InMemoryCollection([
+ new Node('192.168.0.1'),
+ new Node('192.168.0.2'),
+ ]);
+
+ $this->expectException(\UnexpectedValueException::class);
+ $this->expectExceptionMessage('The node "192.168.0.3" hasn\'t been already added.');
+
+ $collection->remove(new Node('192.168.0.3'));
+ }
+
+ public function testSortCollectionByWeightAscending(): void
+ {
+ $collection = new InMemoryCollection([
+ new Node('192.168.0.1', 3),
+ new Node('192.168.0.2', 2),
+ new Node('192.168.0.3', 1),
+ ]);
+
+ $other = $collection->sort(new Asc());
+
+ $expectedResult = [
+ '192.168.0.3',
+ '192.168.0.2',
+ '192.168.0.1',
+ ];
+
+ $this->assertNotSame($collection, $other);
+ $this->assertSame($expectedResult, array_map(static fn (NodeInterface $node): string => $node->getName(), $other->toArray()));
+ }
+
+ public function testSortCollectionByWeightDescending(): void
+ {
+ $collection = new InMemoryCollection([
+ new Node('192.168.0.1', 1),
+ new Node('192.168.0.2', 2),
+ new Node('192.168.0.3', 3),
+ ]);
+
+ $other = $collection->sort(new Desc());
+
+ $expectedResult = [
+ '192.168.0.3',
+ '192.168.0.2',
+ '192.168.0.1',
+ ];
+
+ $this->assertNotSame($collection, $other);
+ $this->assertSame($expectedResult, array_map(static fn (NodeInterface $node): string => $node->getName(), $other->toArray()));
+ }
+
+ public function testCollectionTraversable(): void
+ {
+ $collection = new InMemoryCollection([
+ new Node('192.168.0.1'),
+ new Node('192.168.0.2'),
+ new Node('192.168.0.3'),
+ ]);
+
+ $expectedResult = [
+ '192.168.0.1',
+ '192.168.0.2',
+ '192.168.0.3',
+ ];
+ $actualNodes = [];
+
+ foreach ($collection as $key => $node) {
+ $actualNodes[$key] = $node;
+ }
+
+ $this->assertSame($expectedResult, array_map(static fn (NodeInterface $node): string => $node->getName(), $actualNodes));
+ }
+}
diff --git a/tests/Collection/NodeTest.php b/tests/Collection/NodeTest.php
new file mode 100644
index 0000000..a1fdea4
--- /dev/null
+++ b/tests/Collection/NodeTest.php
@@ -0,0 +1,26 @@
+ 'http://127.0.0.1/',
+ ],
+ );
+
+ $this->assertSame('192.168.0.1', $node->getName());
+ $this->assertSame(5, $node->getWeight());
+ $this->assertSame(['callback_url' => 'http://127.0.0.1/'], $node->getPayload());
+ }
+}
diff --git a/tests/Collection/SorterTest.php b/tests/Collection/SorterTest.php
deleted file mode 100644
index b5a28a8..0000000
--- a/tests/Collection/SorterTest.php
+++ /dev/null
@@ -1,53 +0,0 @@
-sort($collection, new Asc());
-
- $this->assertSame('node1', $collection->getNode(0)->name);
- $this->assertSame('node2', $collection->getNode(1)->name);
- $this->assertSame('node3', $collection->getNode(2)->name);
- $this->assertSame('node4', $collection->getNode(3)->name);
- }
-
- public function testSortDesc(): void
- {
- $collection = new Collection([
- new Node('node2', 8),
- new Node('node3', 16),
- new Node('node1', 4),
- new Node('node4', 32),
- ]);
-
- $sorter = new Sorter();
-
- $sorter->sort($collection, new Desc());
-
- $this->assertSame('node4', $collection->getNode(0)->name);
- $this->assertSame('node3', $collection->getNode(1)->name);
- $this->assertSame('node2', $collection->getNode(2)->name);
- $this->assertSame('node1', $collection->getNode(3)->name);
- }
-}
diff --git a/tests/Counter/InMemoryCounterTest.php b/tests/Counter/InMemoryCounterTest.php
new file mode 100644
index 0000000..21f5753
--- /dev/null
+++ b/tests/Counter/InMemoryCounterTest.php
@@ -0,0 +1,64 @@
+next();
+ }
+
+ $this->assertSame($expectedResult, $actualResult);
+ }
+
+ public function testInMemoryCounterWithStartNumber(): void
+ {
+ $counter = new InMemoryCounter(
+ start: 10,
+ );
+
+ $expectedResult = range(10, 15);
+ $actualResult = [];
+
+ for ($i = 0; $i < 6; ++$i) {
+ $actualResult[] = $counter->next();
+ }
+
+ $this->assertSame($expectedResult, $actualResult);
+ }
+
+ public function testInMemoryCounterWithDifferentNames(): void
+ {
+ $counter = new InMemoryCounter();
+
+ $expectedResult = [
+ 0,
+ 0,
+ 1,
+ 1,
+ 2,
+ 2,
+ ];
+ $actualResult = [];
+
+ for ($i = 0; $i < 6; ++$i) {
+ $actualResult[] = $counter->next(
+ name: 0 === $i % 2 ? 'a' : 'b',
+ );
+ }
+
+ $this->assertSame($expectedResult, $actualResult);
+ }
+}
diff --git a/tests/FrequencyRandomThrottlerTest.php b/tests/FrequencyRandomThrottlerTest.php
new file mode 100644
index 0000000..466b61c
--- /dev/null
+++ b/tests/FrequencyRandomThrottlerTest.php
@@ -0,0 +1,43 @@
+pick($collection);
+
+ if (!isset($actualNodes[$node->getName()])) {
+ $actualNodes[$node->getName()] = 0;
+ }
+
+ ++$actualNodes[$node->getName()];
+ }
+
+ $this->assertCount(3, $actualNodes);
+ $this->assertEquals(1_000, array_sum($actualNodes));
+ $this->assertGreaterThan($actualNodes['192.168.0.2'], $actualNodes['192.168.0.1']);
+ $this->assertGreaterThan($actualNodes['192.168.0.3'], $actualNodes['192.168.0.1']);
+ }
+}
diff --git a/tests/MultipleThrottlerTest.php b/tests/MultipleThrottlerTest.php
new file mode 100644
index 0000000..eb17ee4
--- /dev/null
+++ b/tests/MultipleThrottlerTest.php
@@ -0,0 +1,48 @@
+pick($collection, [
+ 'throttler' => RoundRobinThrottler::class,
+ ]);
+ }
+
+ $this->assertSame($expectedNodes, array_map(static fn (NodeInterface $actualNode): string => $actualNode->getName(), $actualNodes));
+ }
+}
diff --git a/tests/RandomThrottlerTest.php b/tests/RandomThrottlerTest.php
new file mode 100644
index 0000000..524d662
--- /dev/null
+++ b/tests/RandomThrottlerTest.php
@@ -0,0 +1,38 @@
+pick($collection);
+
+ if (!isset($actualNodes[$node->getName()])) {
+ $actualNodes[$node->getName()] = 0;
+ }
+
+ ++$actualNodes[$node->getName()];
+ }
+
+ $this->assertCount(3, $actualNodes);
+ $this->assertEquals(1_000, array_sum($actualNodes));
+ }
+}
diff --git a/tests/RoundRobinThrottlerTest.php b/tests/RoundRobinThrottlerTest.php
new file mode 100644
index 0000000..5db77bb
--- /dev/null
+++ b/tests/RoundRobinThrottlerTest.php
@@ -0,0 +1,41 @@
+pick($collection);
+ }
+
+ $this->assertSame($expectedNodes, array_map(static fn (NodeInterface $actualNode): string => $actualNode->getName(), $actualNodes));
+ }
+}
diff --git a/tests/SmoothWeightedRoundRobinThrottlerTest.php b/tests/SmoothWeightedRoundRobinThrottlerTest.php
new file mode 100644
index 0000000..10454e1
--- /dev/null
+++ b/tests/SmoothWeightedRoundRobinThrottlerTest.php
@@ -0,0 +1,41 @@
+pick($collection);
+ }
+
+ $this->assertSame($expectedNodes, array_map(static fn (NodeInterface $actualNode): string => $actualNode->getName(), $actualNodes));
+ }
+}
diff --git a/tests/Strategy/ClusterDetermineStrategyTest.php b/tests/Strategy/ClusterDetermineStrategyTest.php
deleted file mode 100644
index f300eb6..0000000
--- a/tests/Strategy/ClusterDetermineStrategyTest.php
+++ /dev/null
@@ -1,37 +0,0 @@
-assertSame($expectedNodes[0], $strategy->getNode($collection, ['cluster_name' => 'cluster1']));
- $this->assertSame($expectedNodes[1], $strategy->getNode($collection, ['cluster_name' => 'cluster1']));
- $this->assertSame($expectedNodes[2], $strategy->getNode($collection, ['cluster_name' => 'cluster1']));
- }
-}
diff --git a/tests/Strategy/FrequencyRandomStrategyTest.php b/tests/Strategy/FrequencyRandomStrategyTest.php
deleted file mode 100644
index c09c760..0000000
--- a/tests/Strategy/FrequencyRandomStrategyTest.php
+++ /dev/null
@@ -1,48 +0,0 @@
- new Node('node1'),
- 'node2' => new Node('node2'),
- 'node3' => new Node('node3'),
- ];
-
- $collection = new Collection($nodes);
-
- $strategy = new FrequencyRandomStrategy(frequency: 0.8, depth: 0.2);
-
- $indexes = [];
-
- for ($i = 0; $i < 1000; ++$i) {
- $node = $strategy->getNode($collection);
-
- if (!isset($indexes[$node->name])) {
- $indexes[$node->name] = 0;
- }
-
- ++$indexes[$node->name];
- }
-
- $this->assertCount(3, $indexes);
-
- foreach ($indexes as $count) {
- $this->assertGreaterThan(0, $count);
- }
-
- $this->assertEquals(1000, array_sum($indexes));
- $this->assertGreaterThan($indexes['node2'], $indexes['node1']);
- $this->assertGreaterThan($indexes['node3'], $indexes['node1']);
- }
-}
diff --git a/tests/Strategy/GcdCalculatorTest.php b/tests/Strategy/GcdCalculatorTest.php
deleted file mode 100644
index 0ee8db7..0000000
--- a/tests/Strategy/GcdCalculatorTest.php
+++ /dev/null
@@ -1,18 +0,0 @@
-assertEquals(3, $gcd);
- }
-}
diff --git a/tests/Strategy/InMemoryCounterTest.php b/tests/Strategy/InMemoryCounterTest.php
deleted file mode 100644
index 093a2e2..0000000
--- a/tests/Strategy/InMemoryCounterTest.php
+++ /dev/null
@@ -1,21 +0,0 @@
-assertEquals(5, $counter->next('a'));
- $this->assertEquals(5, $counter->next('b'));
- $this->assertEquals(6, $counter->next('a'));
- $this->assertEquals(6, $counter->next('b'));
- }
-}
diff --git a/tests/Strategy/MultipleDynamicStrategyTest.php b/tests/Strategy/MultipleDynamicStrategyTest.php
deleted file mode 100644
index 33e1c58..0000000
--- a/tests/Strategy/MultipleDynamicStrategyTest.php
+++ /dev/null
@@ -1,36 +0,0 @@
-assertSame($expectedNodes[0], $strategy->getNode($collection, ['strategy_name' => RoundRobinStrategy::class]));
- $this->assertSame($expectedNodes[1], $strategy->getNode($collection, ['strategy_name' => RoundRobinStrategy::class]));
- $this->assertSame($expectedNodes[2], $strategy->getNode($collection, ['strategy_name' => RoundRobinStrategy::class]));
- }
-}
diff --git a/tests/Strategy/RandomStrategyTest.php b/tests/Strategy/RandomStrategyTest.php
deleted file mode 100644
index c306bed..0000000
--- a/tests/Strategy/RandomStrategyTest.php
+++ /dev/null
@@ -1,46 +0,0 @@
- new Node('node1'),
- 'node2' => new Node('node2'),
- 'node3' => new Node('node3'),
- ];
-
- $collection = new Collection($nodes);
-
- $strategy = new RandomStrategy();
-
- $indexes = [];
-
- for ($i = 0; $i < 1000; ++$i) {
- $node = $strategy->getNode($collection);
-
- if (!isset($indexes[$node->name])) {
- $indexes[$node->name] = 0;
- }
-
- ++$indexes[$node->name];
- }
-
- $this->assertCount(3, $indexes);
-
- foreach ($indexes as $count) {
- $this->assertGreaterThan(0, $count);
- }
-
- $this->assertEquals(1000, array_sum($indexes));
- }
-}
diff --git a/tests/Strategy/RoundRobinStrategyTest.php b/tests/Strategy/RoundRobinStrategyTest.php
deleted file mode 100644
index a54659c..0000000
--- a/tests/Strategy/RoundRobinStrategyTest.php
+++ /dev/null
@@ -1,55 +0,0 @@
-
- */
- private array $expectedNodes;
-
- protected function setUp(): void
- {
- $this->expectedNodes = [
- new Node('node1'),
- new Node('node2'),
- new Node('node3'),
- ];
- }
-
- public function testRoundRobin(): InMemoryCounter
- {
- $counter = new InMemoryCounter(start: 0);
- $strategy = new RoundRobinStrategy($counter);
- $collection = new Collection($this->expectedNodes);
-
- $this->assertSame($this->expectedNodes[0], $strategy->getNode($collection));
- $this->assertSame($this->expectedNodes[1], $strategy->getNode($collection));
- $this->assertSame($this->expectedNodes[2], $strategy->getNode($collection));
- $this->assertSame($this->expectedNodes[0], $strategy->getNode($collection));
-
- return $counter;
- }
-
- /**
- * @depends testRoundRobin
- */
- public function testRoundRobinRestart(InMemoryCounter $counter): void
- {
- $strategy = new RoundRobinStrategy($counter);
- $collection = new Collection($this->expectedNodes);
-
- $this->assertSame($this->expectedNodes[1], $strategy->getNode($collection));
- $this->assertSame($this->expectedNodes[2], $strategy->getNode($collection));
- $this->assertSame($this->expectedNodes[0], $strategy->getNode($collection));
- }
-}
diff --git a/tests/Strategy/SmoothWeightedRoundRobinStrategyTest.php b/tests/Strategy/SmoothWeightedRoundRobinStrategyTest.php
deleted file mode 100644
index 744e09b..0000000
--- a/tests/Strategy/SmoothWeightedRoundRobinStrategyTest.php
+++ /dev/null
@@ -1,54 +0,0 @@
-
- */
- private array $expectedNodes;
-
- protected function setUp(): void
- {
- $this->expectedNodes = [
- new Node('node1', 5),
- new Node('node2', 1),
- new Node('node3', 1),
- ];
- }
-
- public function testSmoothWeightedRoundRobin(): string
- {
- $strategy = new SmoothWeightedRoundRobinStrategy();
- $collection = new Collection($this->expectedNodes);
-
- $this->assertSame($this->expectedNodes[0], $strategy->getNode($collection));
- $this->assertSame($this->expectedNodes[0], $strategy->getNode($collection));
- $this->assertSame($this->expectedNodes[1], $strategy->getNode($collection));
- $this->assertSame($this->expectedNodes[0], $strategy->getNode($collection));
-
- return serialize($strategy);
- }
-
- /**
- * @depends testSmoothWeightedRoundRobin
- */
- public function testRestartSmoothWeightedRoundRobin(string $serializedStrategy): void
- {
- /** @var SmoothWeightedRoundRobinStrategy $strategy */
- $strategy = unserialize($serializedStrategy);
- $collection = new Collection($this->expectedNodes);
-
- $this->assertSame($this->expectedNodes[2], $strategy->getNode($collection));
- $this->assertSame($this->expectedNodes[0], $strategy->getNode($collection));
- $this->assertSame($this->expectedNodes[0], $strategy->getNode($collection));
- }
-}
diff --git a/tests/Strategy/WeightedRandomStrategyTest.php b/tests/Strategy/WeightedRandomStrategyTest.php
deleted file mode 100644
index b4b67fc..0000000
--- a/tests/Strategy/WeightedRandomStrategyTest.php
+++ /dev/null
@@ -1,48 +0,0 @@
- new Node('node1', 10),
- 'node2' => new Node('node2', 5),
- 'node3' => new Node('node3', 1),
- ];
-
- $collection = new Collection($nodes);
-
- $strategy = new WeightedRandomStrategy();
-
- $indexes = [];
-
- for ($i = 0; $i < 1000; ++$i) {
- $node = $strategy->getNode($collection);
-
- if (!isset($indexes[$node->name])) {
- $indexes[$node->name] = 0;
- }
-
- ++$indexes[$node->name];
- }
-
- $this->assertCount(3, $indexes);
-
- foreach ($indexes as $count) {
- $this->assertGreaterThan(0, $count);
- }
-
- $this->assertEquals(1000, array_sum($indexes));
- $this->assertGreaterThan($indexes['node2'], $indexes['node1']);
- $this->assertGreaterThan($indexes['node3'], $indexes['node2']);
- }
-}
diff --git a/tests/Strategy/WeightedRoundRobinStrategyTest.php b/tests/Strategy/WeightedRoundRobinStrategyTest.php
deleted file mode 100644
index 185cde6..0000000
--- a/tests/Strategy/WeightedRoundRobinStrategyTest.php
+++ /dev/null
@@ -1,56 +0,0 @@
-
- */
- private array $expectedNodes;
-
- protected function setUp(): void
- {
- $this->expectedNodes = [
- new Node('node1', 5),
- new Node('node2', 1),
- new Node('node3', 1),
- ];
- }
-
- public function testWeightedRoundRobin(): InMemoryCounter
- {
- $counter = new InMemoryCounter(start: 0);
- $strategy = new WeightedRoundRobinStrategy($counter);
- $collection = new Collection($this->expectedNodes);
-
- $this->assertSame($this->expectedNodes[0], $strategy->getNode($collection));
- $this->assertSame($this->expectedNodes[0], $strategy->getNode($collection));
- $this->assertSame($this->expectedNodes[0], $strategy->getNode($collection));
- $this->assertSame($this->expectedNodes[0], $strategy->getNode($collection));
- $this->assertSame($this->expectedNodes[0], $strategy->getNode($collection));
-
- return $counter;
- }
-
- /**
- * @depends testWeightedRoundRobin
- */
- public function testRestartWeightedRoundRobin(InMemoryCounter $counter): void
- {
- $strategy = new WeightedRoundRobinStrategy($counter);
- $collection = new Collection($this->expectedNodes);
-
- $this->assertSame($this->expectedNodes[1], $strategy->getNode($collection));
- $this->assertSame($this->expectedNodes[2], $strategy->getNode($collection));
- $this->assertSame($this->expectedNodes[0], $strategy->getNode($collection));
- }
-}
diff --git a/tests/ThrottlerTest.php b/tests/ThrottlerTest.php
deleted file mode 100644
index 5161751..0000000
--- a/tests/ThrottlerTest.php
+++ /dev/null
@@ -1,29 +0,0 @@
-assertSame($node, $throttler->pick($collection));
- }
-}
diff --git a/tests/WeightedRandomThrottlerTest.php b/tests/WeightedRandomThrottlerTest.php
new file mode 100644
index 0000000..114fd4a
--- /dev/null
+++ b/tests/WeightedRandomThrottlerTest.php
@@ -0,0 +1,40 @@
+pick($collection);
+
+ if (!isset($actualNodes[$node->getName()])) {
+ $actualNodes[$node->getName()] = 0;
+ }
+
+ ++$actualNodes[$node->getName()];
+ }
+
+ $this->assertCount(3, $actualNodes);
+ $this->assertEquals(1_000, array_sum($actualNodes));
+ $this->assertGreaterThan($actualNodes['192.168.0.2'], $actualNodes['192.168.0.1']);
+ $this->assertGreaterThan($actualNodes['192.168.0.3'], $actualNodes['192.168.0.2']);
+ }
+}
diff --git a/tests/WeightedRoundRobinThrottlerTest.php b/tests/WeightedRoundRobinThrottlerTest.php
new file mode 100644
index 0000000..90da41e
--- /dev/null
+++ b/tests/WeightedRoundRobinThrottlerTest.php
@@ -0,0 +1,42 @@
+pick($collection);
+ }
+
+ $this->assertSame($expectedNodes, array_map(static fn (NodeInterface $actualNode): string => $actualNode->getName(), $actualNodes));
+ }
+}