From 271dc11d913bc6bee293ea9e48684e66832e7c99 Mon Sep 17 00:00:00 2001 From: Aleksandr Denisyuk Date: Thu, 28 Dec 2023 00:51:15 +0300 Subject: [PATCH 01/20] Upgrade package --- .github/workflows/ci.yml | 24 +- Dockerfile | 11 + LICENSE | 2 +- Makefile | 5 + README.md | 81 ++-- benchmarks/FrequencyRandomBench.php | 13 +- benchmarks/RandomBench.php | 13 +- benchmarks/RoundRobinBench.php | 17 +- benchmarks/SmoothWeightedRoundRobinBench.php | 13 +- benchmarks/WeightedRandomBench.php | 13 +- benchmarks/WeightedRoundRobinBench.php | 17 +- composer.json | 16 +- docs/index.md | 409 ++++++++---------- helpers.php | 21 + psalm-baseline.xml | 80 +--- src/{Collection => Cluster}/Cluster.php | 8 +- .../ClusterInterface.php | 5 +- src/Cluster/ClusterPool.php | 56 +++ src/Cluster/ClusterSet.php | 19 + src/Collection/Asc.php | 13 - src/Collection/Collection.php | 92 ---- src/Collection/CollectionInterface.php | 14 +- src/Collection/Desc.php | 13 - .../Exception/EmptyCollectionException.php | 13 - .../UnweightedCollectionException.php | 13 - src/Collection/InMemoryCollection.php | 105 +++++ src/Collection/Node.php | 29 +- src/Collection/NodeInterface.php | 17 + src/Collection/Sort/Asc.php | 15 + src/Collection/Sort/Desc.php | 15 + src/Collection/Sorter.php | 21 - src/Counter/CounterInterface.php | 10 + src/Counter/InMemoryCounter.php | 25 ++ src/FrequencyRandomThrottler.php | 32 ++ src/MultipleThrottler.php | 40 ++ src/RandomThrottler.php | 22 + src/RoundRobinThrottler.php | 29 ++ src/SmoothWeightedRoundRobinThrottler.php | 50 +++ src/Strategy/ClusterDetermineStrategy.php | 51 --- src/Strategy/ClusterSet.php | 20 - src/Strategy/CounterInterface.php | 10 - src/Strategy/FrequencyRandomStrategy.php | 38 -- src/Strategy/GcdCalculator.php | 19 - src/Strategy/InMemoryCounter.php | 27 -- src/Strategy/MultipleDynamicStrategy.php | 43 -- src/Strategy/RandomStrategy.php | 23 - src/Strategy/RoundRobinStrategy.php | 28 -- .../SmoothWeightedRoundRobinStrategy.php | 68 --- src/Strategy/StrategyInterface.php | 13 - src/Strategy/WeightedRandomStrategy.php | 57 --- src/Strategy/WeightedRoundRobinStrategy.php | 85 ---- src/Throttler.php | 22 - src/ThrottlerInterface.php | 4 +- src/WeightedRandomThrottler.php | 36 ++ src/WeightedRoundRobinThrottler.php | 56 +++ tests/.gitkeep | 0 tests/Collection/ClusterTest.php | 35 -- tests/Collection/CollectionTest.php | 171 -------- tests/Collection/SorterTest.php | 53 --- .../Strategy/ClusterDetermineStrategyTest.php | 37 -- .../Strategy/FrequencyRandomStrategyTest.php | 48 -- tests/Strategy/GcdCalculatorTest.php | 18 - tests/Strategy/InMemoryCounterTest.php | 21 - .../Strategy/MultipleDynamicStrategyTest.php | 36 -- tests/Strategy/RandomStrategyTest.php | 46 -- tests/Strategy/RoundRobinStrategyTest.php | 55 --- .../SmoothWeightedRoundRobinStrategyTest.php | 54 --- tests/Strategy/WeightedRandomStrategyTest.php | 48 -- .../WeightedRoundRobinStrategyTest.php | 56 --- tests/ThrottlerTest.php | 29 -- 70 files changed, 886 insertions(+), 1812 deletions(-) create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 helpers.php rename src/{Collection => Cluster}/Cluster.php (66%) rename src/{Collection => Cluster}/ClusterInterface.php (58%) create mode 100644 src/Cluster/ClusterPool.php create mode 100644 src/Cluster/ClusterSet.php delete mode 100644 src/Collection/Asc.php delete mode 100644 src/Collection/Collection.php delete mode 100644 src/Collection/Desc.php delete mode 100644 src/Collection/Exception/EmptyCollectionException.php delete mode 100644 src/Collection/Exception/UnweightedCollectionException.php create mode 100644 src/Collection/InMemoryCollection.php create mode 100644 src/Collection/NodeInterface.php create mode 100644 src/Collection/Sort/Asc.php create mode 100644 src/Collection/Sort/Desc.php delete mode 100644 src/Collection/Sorter.php create mode 100644 src/Counter/CounterInterface.php create mode 100644 src/Counter/InMemoryCounter.php create mode 100644 src/FrequencyRandomThrottler.php create mode 100644 src/MultipleThrottler.php create mode 100644 src/RandomThrottler.php create mode 100644 src/RoundRobinThrottler.php create mode 100644 src/SmoothWeightedRoundRobinThrottler.php delete mode 100644 src/Strategy/ClusterDetermineStrategy.php delete mode 100644 src/Strategy/ClusterSet.php delete mode 100644 src/Strategy/CounterInterface.php delete mode 100644 src/Strategy/FrequencyRandomStrategy.php delete mode 100644 src/Strategy/GcdCalculator.php delete mode 100644 src/Strategy/InMemoryCounter.php delete mode 100644 src/Strategy/MultipleDynamicStrategy.php delete mode 100644 src/Strategy/RandomStrategy.php delete mode 100644 src/Strategy/RoundRobinStrategy.php delete mode 100644 src/Strategy/SmoothWeightedRoundRobinStrategy.php delete mode 100644 src/Strategy/StrategyInterface.php delete mode 100644 src/Strategy/WeightedRandomStrategy.php delete mode 100644 src/Strategy/WeightedRoundRobinStrategy.php delete mode 100644 src/Throttler.php create mode 100644 src/WeightedRandomThrottler.php create mode 100644 src/WeightedRoundRobinThrottler.php create mode 100644 tests/.gitkeep delete mode 100644 tests/Collection/ClusterTest.php delete mode 100644 tests/Collection/CollectionTest.php delete mode 100644 tests/Collection/SorterTest.php delete mode 100644 tests/Strategy/ClusterDetermineStrategyTest.php delete mode 100644 tests/Strategy/FrequencyRandomStrategyTest.php delete mode 100644 tests/Strategy/GcdCalculatorTest.php delete mode 100644 tests/Strategy/InMemoryCounterTest.php delete mode 100644 tests/Strategy/MultipleDynamicStrategyTest.php delete mode 100644 tests/Strategy/RandomStrategyTest.php delete mode 100644 tests/Strategy/RoundRobinStrategyTest.php delete mode 100644 tests/Strategy/SmoothWeightedRoundRobinStrategyTest.php delete mode 100644 tests/Strategy/WeightedRandomStrategyTest.php delete mode 100644 tests/Strategy/WeightedRoundRobinStrategyTest.php delete mode 100644 tests/ThrottlerTest.php 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/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..02e9ea6 100644 --- a/README.md +++ b/README.md @@ -20,38 +20,60 @@ 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 as the second argument in constructor if you are using weighted strategies: ```php pick($collection); - + // ... } ``` -Set weight for Node as the second argument in constructor if you are using weighted-strategies. +As a result, the strategy will go through all the nodes and return the appropriate one like below: + +```text ++-------------+ +| 192.168.0.1 | +| 192.168.0.1 | +| 192.168.0.1 | +| 192.168.0.1 | +| 192.168.0.1 | +| 192.168.0.2 | +| 192.168.0.3 | +| etc. | ++-------------+ +``` + +The following load balancing strategies 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 +83,26 @@ 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) +- [How it works](docs/index.md##how-it-works) +- [Available strategies](docs/index.md##available-strategies) +- [Keep states](docs/index.md##keep-states) + - [Counting](docs/index.md##counting) + - [Serialization](docs/index.md##serialization) +- [Choice from multiple](docs/index.md##choice-from-multiple) +- [Balance cluster](docs/index.md##balance-cluster) +- [Production example](docs/index.md##production-example) -Read more about usage on [Orangesoft Tech](https://orangesoft.co/blog/how-to-make-proxy-balancing-in-guzzle). +Read more about [Load Balancing](https://samwho.dev/load-balancing/). diff --git a/benchmarks/FrequencyRandomBench.php b/benchmarks/FrequencyRandomBench.php index 4b7e47b..0c8a545 100644 --- a/benchmarks/FrequencyRandomBench.php +++ b/benchmarks/FrequencyRandomBench.php @@ -4,29 +4,26 @@ 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([ + $this->collection = new InMemoryCollection([ new Node('node1'), new Node('node2'), new Node('node3'), ]); - $this->throttler = new Throttler( - new FrequencyRandomStrategy(), - ); + $this->throttler = new FrequencyRandomThrottler(); } /** diff --git a/benchmarks/RandomBench.php b/benchmarks/RandomBench.php index 8ea5b62..1fb934d 100644 --- a/benchmarks/RandomBench.php +++ b/benchmarks/RandomBench.php @@ -4,29 +4,26 @@ 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([ + $this->collection = new InMemoryCollection([ new Node('node1'), new Node('node2'), new Node('node3'), ]); - $this->throttler = new Throttler( - new RandomStrategy(), - ); + $this->throttler = new RandomThrottler(); } /** diff --git a/benchmarks/RoundRobinBench.php b/benchmarks/RoundRobinBench.php index a6deaa8..02ad98a 100644 --- a/benchmarks/RoundRobinBench.php +++ b/benchmarks/RoundRobinBench.php @@ -4,31 +4,28 @@ 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([ + $this->collection = new InMemoryCollection([ new Node('node1'), new Node('node2'), new Node('node3'), ]); - $this->throttler = new Throttler( - new RoundRobinStrategy( - new InMemoryCounter(start: 0), - ) + $this->throttler = new RoundRobinThrottler( + new InMemoryCounter(), ); } diff --git a/benchmarks/SmoothWeightedRoundRobinBench.php b/benchmarks/SmoothWeightedRoundRobinBench.php index 896d7dc..649295b 100644 --- a/benchmarks/SmoothWeightedRoundRobinBench.php +++ b/benchmarks/SmoothWeightedRoundRobinBench.php @@ -4,29 +4,26 @@ 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([ + $this->collection = new InMemoryCollection([ new Node('node1', 5), new Node('node2', 1), new Node('node3', 1), ]); - $this->throttler = new Throttler( - new SmoothWeightedRoundRobinStrategy(), - ); + $this->throttler = new SmoothWeightedRoundRobinThrottler(); } /** diff --git a/benchmarks/WeightedRandomBench.php b/benchmarks/WeightedRandomBench.php index ac0a0a1..8927cde 100644 --- a/benchmarks/WeightedRandomBench.php +++ b/benchmarks/WeightedRandomBench.php @@ -4,29 +4,26 @@ 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([ + $this->collection = new InMemoryCollection([ new Node('node1', 5), new Node('node2', 1), new Node('node3', 1), ]); - $this->throttler = new Throttler( - new WeightedRandomStrategy(), - ); + $this->throttler = new WeightedRandomThrottler(); } /** diff --git a/benchmarks/WeightedRoundRobinBench.php b/benchmarks/WeightedRoundRobinBench.php index 9a2ee6e..ed7a067 100644 --- a/benchmarks/WeightedRoundRobinBench.php +++ b/benchmarks/WeightedRoundRobinBench.php @@ -4,31 +4,28 @@ 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([ + $this->collection = new InMemoryCollection([ new Node('node1', 5), new Node('node2', 1), new Node('node3', 1), ]); - $this->throttler = new Throttler( - new WeightedRoundRobinStrategy( - new InMemoryCounter(start: 0), - ) + $this->throttler = new WeightedRoundRobinThrottler( + new InMemoryCounter(), ); } 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..a7191aa 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,201 +1,75 @@ # Documentation -- [Configure Throttler](#configure-throttler) +- [How it works](#how-it-works) - [Available strategies](#available-strategies) -- [Sort nodes](#sort-nodes) -- [Keep counter](#keep-counter) -- [Serialize strategies](#serialize-strategies) -- [Dynamically change strategy](#dynamically-change-strategy) +- [Keep states](#keep-states) + - [Counting](#counting) + - [Serialization](#serialization) +- [Choice from multiple](#choice-from-multiple) - [Balance cluster](#balance-cluster) +- [Production example](#production-example) -## Configure Throttler +## How it works -You need to choose a strategy and configure it: - -```php -pick($collection); - - // ... -} -``` - -As a result, you will see the following distribution of nodes: - -```text -+-------+ -| node1 | -| node1 | -| node1 | -| node1 | -| node1 | -| node2 | -| node3 | -| etc. | -+-------+ -``` - -Result of Throttler depends on the chosen strategy. +[...] ## Available strategies -Strategies are divided into two types: random and round-robin. The following strategies are available: - -- [RandomStrategy](../src/Strategy/RandomStrategy.php) -- [WeightedRandomStrategy](../src/Strategy/WeightedRandomStrategy.php) -- [FrequencyRandomStrategy](../src/Strategy/FrequencyRandomStrategy.php) -- [RoundRobinStrategy](../src/Strategy/RoundRobinStrategy.php) -- [WeightedRoundRobinStrategy](../src/Strategy/WeightedRoundRobinStrategy.php) -- [SmoothWeightedRoundRobinStrategy](../src/Strategy/SmoothWeightedRoundRobinStrategy.php) -- [MultipleDynamicStrategy](../src/Strategy/MultipleDynamicStrategy.php) -- [ClusterDetermineStrategy](../src/Strategy/ClusterDetermineStrategy.php) - -## 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: - -```php -sort($collection, new Desc()); -``` +## Keep states -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: +[...] ```text -+--------+--------+ -| name | weight | -+--------+--------+ -| node7 | 2048 | -| node4 | 1024 | -| node3 | 512 | -| node8 | 256 | -| node5 | 128 | -| node6 | 64 | -| node1 | 32 | -| node2 | 16 | -| node9 | 8 | -| node10 | 4 | -+--------+--------+ ++--------------------------+---------------+ +| Throttler | Method | ++--------------------------+---------------+ +| Random | [x] | +| WeightedRandom | [x] | +| FrequencyRandom | [x] | +| RoundRobin | counting | +| WeightedRoundRobin | counting | +| SmoothWeightedRoundRobin | serialization | ++--------------------------+---------------+ ``` -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: +[...] -```php -$throttler = new Throttler( - new FrequencyRandomStrategy(frequency: 0.8, depth: 0.2), -); - -/** @var Node $node */ -$node = $throttler->pick($collection); -``` +### Counting -The probability of choosing nodes for FrequencyRandomStrategy can be visualized as follows: +[...] ```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% | -+--------+--------+ +composer require predis/predis ``` -If you need the reverse order of the nodes use Asc direction. - -## Keep counter - -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: +[...] ```php client->exists($name)) { - $this->client->set($name, -1); + $this->client->set($name, $start - 1); } return $this->client->incr($name); @@ -203,127 +77,200 @@ class RedisCounter implements CounterInterface } ``` -In the example above, we wrote the counter with Redis. +[...] ```php /** @var Predis\Client $client */ +$client = new Client('tcp://127.0.0.1:6379'); -$strategy = new WeightedRoundRobinStrategy( +$throttler = new WeightedRoundRobinThrottler( new RedisCounter($client), ); ``` -Now Throttler will resume work from the last node according to the chosen strategy. +[...] -## Serialize strategies +### Serialization -[SmoothWeightedRoundRobinStrategy](../src/Strategy/SmoothWeightedRoundRobinStrategy.php) does not support counters. Instead it you can serialize and unserialize this strategy to keep last state: +[...] + + + +## Choice from multiple + +[...] ```php pick($collection, [ + 'throttler' => RoundRobinStrategy::class, +]); ``` -This way you can preserve the order of nodes for a given strategy between PHP calls. +[...] -## Dynamically change strategy +## Balance cluster -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: +[...] ```php pick($collection, [ - 'strategy_name' => RoundRobinStrategy::class, -]); +/** @var NodeInterface $node */ +$node = $cluster->balance($pool); ``` -The advantage of this method is that you do not need to create many instances of the balancer. +[...] -## Balance cluster +## Production example + +[...] -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: +```text +composer require \ + && orangesoft/throttler \ + && guzzlehttp/guzzle \ + && psr/http-message +``` + +[...] ```php $context + */ + public function __construct( + private readonly ThrottlerInterface $throttler, + private readonly CollectionInterface $collection, + private array $context = [], + ) { + } -$throttler = new Throttler( - new ClusterDetermineStrategy( - new ClusterSet(new RoundRobinStrategy(new InMemoryCounter(start: 0)), ['cluster1']), - new ClusterSet(new RandomStrategy(), ['cluster2', 'cluster3']), - ) -); + 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 clusters from nodes: +[...] ```php -$collection = new Collection([ - new Node('node1'), - new Node('node2'), - new Node('node3'), +$throttler = new WeightedRoundRobinThrottler( + new InMemoryCounter(), +); + +$collection = new InMemoryCollection([ + new Node('user:pass@192.168.0.1', 5), + new Node('user:pass@192.168.0.2', 1), + new Node('user:pass@192.168.0.3', 1), ]); -$cluster = new Cluster('cluster1', $collection); +$stack = HandlerStack::create(); +$stack->push(new ProxyMiddleware($throttler, $collection)); +$client = new Client(['handler' => $stack]); ``` -Using the `balance()` method, force your cluster to balance: +[...] ```php -/** @var Node $node */ -$node = $cluster->balance($throttler); +while (true) { + /** @var ResponseInterface $response */ + $response = $client->get('https://httpbin.org/ip'); + + // ... +} +``` + +[...] + +```text ++-------------+ +| 192.168.0.1 | +| 192.168.0.1 | +| 192.168.0.1 | +| 192.168.0.1 | +| 192.168.0.1 | +| 192.168.0.2 | +| 192.168.0.3 | +| 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. +[...] 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/Collection/Cluster.php b/src/Cluster/Cluster.php similarity index 66% rename from src/Collection/Cluster.php rename to src/Cluster/Cluster.php index b4497fd..7699cdf 100644 --- a/src/Collection/Cluster.php +++ b/src/Cluster/Cluster.php @@ -2,8 +2,10 @@ declare(strict_types=1); -namespace Orangesoft\Throttler\Collection; +namespace Orangesoft\Throttler\Cluster; +use Orangesoft\Throttler\Collection\CollectionInterface; +use Orangesoft\Throttler\Collection\NodeInterface; use Orangesoft\Throttler\ThrottlerInterface; final class Cluster implements ClusterInterface @@ -14,10 +16,10 @@ public function __construct( ) { } - public function balance(ThrottlerInterface $throttler, array $context = []): Node + public function balance(ThrottlerInterface $throttler, array $context = []): NodeInterface { return $throttler->pick($this->collection, array_merge($context, [ - 'cluster_name' => $this->name, + 'cluster' => $this->name, ])); } } diff --git a/src/Collection/ClusterInterface.php b/src/Cluster/ClusterInterface.php similarity index 58% rename from src/Collection/ClusterInterface.php rename to src/Cluster/ClusterInterface.php index 1cd6a41..2141b40 100644 --- a/src/Collection/ClusterInterface.php +++ b/src/Cluster/ClusterInterface.php @@ -2,11 +2,12 @@ declare(strict_types=1); -namespace Orangesoft\Throttler\Collection; +namespace Orangesoft\Throttler\Cluster; +use Orangesoft\Throttler\Collection\NodeInterface; use Orangesoft\Throttler\ThrottlerInterface; interface ClusterInterface { - public function balance(ThrottlerInterface $throttler, array $context = []): Node; + public function balance(ThrottlerInterface $throttler, array $context = []): NodeInterface; } diff --git a/src/Cluster/ClusterPool.php b/src/Cluster/ClusterPool.php new file mode 100644 index 0000000..f9c243b --- /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('Cluster "%s" has already been added.', $clusterName)); + } + + $this->clusterNames[$clusterName] = $id; + } + } + } + + public function pick(CollectionInterface $collection, array $context = []): NodeInterface + { + if (!isset($context['cluster'])) { + throw new \RuntimeException('The parameter "cluster" is required.'); + } + + if (!\is_string($context['cluster'])) { + throw new \RuntimeException('The parameter "cluster" must be as a string.'); + } + + if (!isset($this->clusterNames[$context['cluster']])) { + throw new \RuntimeException(sprintf('The cluster "%s" is undefined.', $context['cluster'])); + } + + $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..b5197f8 --- /dev/null +++ b/src/Cluster/ClusterSet.php @@ -0,0 +1,19 @@ +weight - $b->weight; - } -} diff --git a/src/Collection/Collection.php b/src/Collection/Collection.php deleted file mode 100644 index e735159..0000000 --- a/src/Collection/Collection.php +++ /dev/null @@ -1,92 +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..2dfba96 100644 --- a/src/Collection/CollectionInterface.php +++ b/src/Collection/CollectionInterface.php @@ -6,22 +6,20 @@ 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; + public function sort(callable $callback): self; public function isEmpty(): bool; /** - * @return Node[] + * @return NodeInterface[] */ 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()); + } + + public function sort(callable $callback): self + { + $nodes = $this->toArray(); + usort($nodes, $callback); + + return new self($nodes); + } + + public function isEmpty(): bool + { + return 0 == $this->count(); + } + + public function count(): int + { + return \count($this->nodes); + } + + /** + * @return NodeInterface[] + */ + public function toArray(): array + { + return $this->nodes; + } + + public function getIterator(): \Traversable + { + 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/Counter/CounterInterface.php b/src/Counter/CounterInterface.php new file mode 100644 index 0000000..62c3705 --- /dev/null +++ b/src/Counter/CounterInterface.php @@ -0,0 +1,10 @@ + + */ + private array $counter = []; + + public function next(string $name = 'default', int $start = 0): int + { + if (!isset($this->counter[$name])) { + $this->counter[$name] = $start; + } + + $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..07dadf1 --- /dev/null +++ b/src/FrequencyRandomThrottler.php @@ -0,0 +1,32 @@ +isEmpty()) { + throw new \RuntimeException('Collection of nodes mustn\'t be empty.'); + } + + $total = \count($collection); + $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 $collection->get($key - 1); + } +} diff --git a/src/MultipleThrottler.php b/src/MultipleThrottler.php new file mode 100644 index 0000000..427d8f6 --- /dev/null +++ b/src/MultipleThrottler.php @@ -0,0 +1,40 @@ +throttlers[$throttler::class] = $throttler; + } + } + + public function pick(CollectionInterface $collection, array $context = []): NodeInterface + { + if (!isset($context['throttler'])) { + throw new \RuntimeException('Required parameter "throttler" is missing.'); + } + + if (!isset($this->throttlers[$context['throttler']])) { + throw new \RuntimeException(sprintf('Throttler "%s" is undefined.', $context['throttler'])); + } + + if (!class_exists($context['throttler']) || !is_a($context['throttler'], ThrottlerInterface::class, true)) { + throw new \UnexpectedValueException(sprintf('Throttler must be a class that exists and implements "%s" interface, "%s" given.', ThrottlerInterface::class, get_debug_type($context['throttler']))); + } + + /** @var ThrottlerInterface $throttler */ + $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..ee593fe --- /dev/null +++ b/src/RandomThrottler.php @@ -0,0 +1,22 @@ +isEmpty()) { + throw new \RuntimeException('Collection of nodes mustn\'t be empty.'); + } + + $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..b270ed9 --- /dev/null +++ b/src/RoundRobinThrottler.php @@ -0,0 +1,29 @@ +isEmpty()) { + throw new \RuntimeException('Collection of nodes mustn\'t be empty.'); + } + + $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..5d7feb4 --- /dev/null +++ b/src/SmoothWeightedRoundRobinThrottler.php @@ -0,0 +1,50 @@ +> + */ + private array $weights = []; + /** + * @var array> + */ + private array $currentWeights = []; + + public function pick(CollectionInterface $collection, array $context = []): NodeInterface + { + if ($collection->isEmpty()) { + throw new \RuntimeException('Collection of nodes mustn\'t be empty.'); + } + + $counter = $context['counter'] ?? spl_object_hash($collection); + + if (!isset($this->weights[$counter]) || !isset($this->currentWeights[$counter])) { + foreach ($collection as $key => $node) { + if (0 == $node->getWeight()) { + throw new \RuntimeException('All nodes in the collection must be weighted.'); + } + + $this->weights[$counter][$key] = $this->currentWeights[$counter][$key] = $node->getWeight(); + } + } + + uasort($this->currentWeights[$counter], static fn (int $a, int $b): int => $a <=> $b); + $sumWeights = array_sum($this->weights[$counter]); + $maxCurrentWeightKey = array_key_last($this->currentWeights[$counter]); + $this->currentWeights[$counter][$maxCurrentWeightKey] -= $sumWeights; + + 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 $counter = []; - - public function __construct( - private int $start = 0, - ) { - } - - public function next(string $name = 'default'): int - { - if (!isset($this->counter[$name])) { - $this->counter[$name] = $this->start; - } - - return $this->counter[$name]++; - } -} diff --git a/src/Strategy/MultipleDynamicStrategy.php b/src/Strategy/MultipleDynamicStrategy.php deleted file mode 100644 index b6580f2..0000000 --- a/src/Strategy/MultipleDynamicStrategy.php +++ /dev/null @@ -1,43 +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..0cd3393 100644 --- a/src/ThrottlerInterface.php +++ b/src/ThrottlerInterface.php @@ -5,9 +5,9 @@ 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; + public function pick(CollectionInterface $collection, array $context = []): NodeInterface; } diff --git a/src/WeightedRandomThrottler.php b/src/WeightedRandomThrottler.php new file mode 100644 index 0000000..e23c2da --- /dev/null +++ b/src/WeightedRandomThrottler.php @@ -0,0 +1,36 @@ +isEmpty()) { + throw new \RuntimeException('Collection of nodes mustn\'t be empty.'); + } + + $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.'); + } + + $currentWeight += $node->getWeight(); + + if ($randomWeight <= $currentWeight) { + return $node; + } + } + + throw new \RuntimeException('You never will catch this exception.'); + } +} diff --git a/src/WeightedRoundRobinThrottler.php b/src/WeightedRoundRobinThrottler.php new file mode 100644 index 0000000..eba560f --- /dev/null +++ b/src/WeightedRoundRobinThrottler.php @@ -0,0 +1,56 @@ +isEmpty()) { + throw new \RuntimeException('Collection of nodes mustn\'t be empty.'); + } + + $gcdWeight = 0; + $maxWeight = 0; + $currentWeight = 0; + + foreach ($collection as $node) { + if (0 == $node->getWeight()) { + throw new \RuntimeException('All nodes in the collection must be weighted.'); + } + + $gcdWeight = gcd($gcdWeight, $node->getWeight()); + $maxWeight = max($maxWeight, $node->getWeight()); + } + + while (true) { + $counter = $context['counter'] ?? spl_object_hash($collection); + $key = $this->counter->next($counter) % \count($collection); + + if (0 == $key) { + $currentWeight -= $gcdWeight; + + if (0 >= $currentWeight) { + $currentWeight = $maxWeight; + } + } + + $node = $collection->get($key); + + if ($node->getWeight() >= $currentWeight) { + return $node; + } + } + } +} diff --git a/tests/.gitkeep b/tests/.gitkeep new file mode 100644 index 0000000..e69de29 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/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/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)); - } -} From 33cd9950fe883dde4722c6a45684273ef6e2df96 Mon Sep 17 00:00:00 2001 From: Aleksandr Denisyuk Date: Wed, 12 Jun 2024 16:03:00 +0300 Subject: [PATCH 02/20] Update php-cs-fixer rules --- .php-cs-fixer.dist.php | 2 ++ tests/.gitkeep | 0 2 files changed, 2 insertions(+) delete mode 100644 tests/.gitkeep 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/tests/.gitkeep b/tests/.gitkeep deleted file mode 100644 index e69de29..0000000 From 6635aed3d0d3a7863a4a7d6271abc9ed5524e239 Mon Sep 17 00:00:00 2001 From: Aleksandr Denisyuk Date: Wed, 12 Jun 2024 16:03:21 +0300 Subject: [PATCH 03/20] Update benchmarks --- benchmarks/FrequencyRandomBench.php | 9 ++++----- benchmarks/RandomBench.php | 9 ++++----- benchmarks/RoundRobinBench.php | 13 +++++-------- benchmarks/SmoothWeightedRoundRobinBench.php | 9 ++++----- benchmarks/WeightedRandomBench.php | 9 ++++----- benchmarks/WeightedRoundRobinBench.php | 13 +++++-------- 6 files changed, 26 insertions(+), 36 deletions(-) diff --git a/benchmarks/FrequencyRandomBench.php b/benchmarks/FrequencyRandomBench.php index 0c8a545..d7f9717 100644 --- a/benchmarks/FrequencyRandomBench.php +++ b/benchmarks/FrequencyRandomBench.php @@ -18,9 +18,9 @@ final class FrequencyRandomBench public function __construct() { $this->collection = new InMemoryCollection([ - new Node('node1'), - new Node('node2'), - new Node('node3'), + new Node('192.168.0.1'), + new Node('192.168.0.2'), + new Node('192.168.0.3'), ]); $this->throttler = new FrequencyRandomThrottler(); @@ -28,10 +28,9 @@ public function __construct() /** * @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 1fb934d..571b5dd 100644 --- a/benchmarks/RandomBench.php +++ b/benchmarks/RandomBench.php @@ -18,9 +18,9 @@ final class RandomBench public function __construct() { $this->collection = new InMemoryCollection([ - new Node('node1'), - new Node('node2'), - new Node('node3'), + new Node('192.168.0.1'), + new Node('192.168.0.2'), + new Node('192.168.0.3'), ]); $this->throttler = new RandomThrottler(); @@ -28,10 +28,9 @@ public function __construct() /** * @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 02ad98a..f4fde43 100644 --- a/benchmarks/RoundRobinBench.php +++ b/benchmarks/RoundRobinBench.php @@ -19,22 +19,19 @@ final class RoundRobinBench public function __construct() { $this->collection = new InMemoryCollection([ - new Node('node1'), - new Node('node2'), - new Node('node3'), + new Node('192.168.0.1'), + new Node('192.168.0.2'), + new Node('192.168.0.3'), ]); - $this->throttler = new RoundRobinThrottler( - new InMemoryCounter(), - ); + $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 649295b..148617c 100644 --- a/benchmarks/SmoothWeightedRoundRobinBench.php +++ b/benchmarks/SmoothWeightedRoundRobinBench.php @@ -18,9 +18,9 @@ final class SmoothWeightedRoundRobinBench public function __construct() { $this->collection = new InMemoryCollection([ - new Node('node1', 5), - new Node('node2', 1), - new Node('node3', 1), + new Node('192.168.0.1', 5), + new Node('192.168.0.2', 1), + new Node('192.168.0.3', 1), ]); $this->throttler = new SmoothWeightedRoundRobinThrottler(); @@ -28,10 +28,9 @@ public function __construct() /** * @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 8927cde..a242ca4 100644 --- a/benchmarks/WeightedRandomBench.php +++ b/benchmarks/WeightedRandomBench.php @@ -18,9 +18,9 @@ final class WeightedRandomBench public function __construct() { $this->collection = new InMemoryCollection([ - new Node('node1', 5), - new Node('node2', 1), - new Node('node3', 1), + new Node('192.168.0.1', 5), + new Node('192.168.0.2', 1), + new Node('192.168.0.3', 1), ]); $this->throttler = new WeightedRandomThrottler(); @@ -28,10 +28,9 @@ public function __construct() /** * @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 ed7a067..8b99314 100644 --- a/benchmarks/WeightedRoundRobinBench.php +++ b/benchmarks/WeightedRoundRobinBench.php @@ -19,22 +19,19 @@ final class WeightedRoundRobinBench public function __construct() { $this->collection = new InMemoryCollection([ - new Node('node1', 5), - new Node('node2', 1), - new Node('node3', 1), + new Node('192.168.0.1', 5), + new Node('192.168.0.2', 1), + new Node('192.168.0.3', 1), ]); - $this->throttler = new WeightedRoundRobinThrottler( - new InMemoryCounter(), - ); + $this->throttler = new WeightedRoundRobinThrottler(new InMemoryCounter()); } /** * @Revs(1000) - * * @Iterations(5) */ - public function benchWeightedRoundRobin(): void + public function benchWeightedRoundRobinAlgorithm(): void { $this->throttler->pick($this->collection); } From 29a9ac0813d004364fd9bd6da2dea9844ed3e196 Mon Sep 17 00:00:00 2001 From: Aleksandr Denisyuk Date: Wed, 12 Jun 2024 16:03:43 +0300 Subject: [PATCH 04/20] Refactor throttlers --- src/Cluster/Cluster.php | 3 ++ src/Cluster/ClusterInterface.php | 3 ++ src/Cluster/ClusterPool.php | 8 ++--- src/Cluster/ClusterSet.php | 3 ++ src/Collection/CollectionInterface.php | 9 +++++- src/Collection/InMemoryCollection.php | 18 ++++++++++-- src/Counter/CounterInterface.php | 2 +- src/Counter/InMemoryCounter.php | 9 ++++-- src/FrequencyRandomThrottler.php | 5 +++- src/MultipleThrottler.php | 15 ++++++---- src/RandomThrottler.php | 5 +++- src/RoundRobinThrottler.php | 9 +++++- src/SmoothWeightedRoundRobinThrottler.php | 36 ++++++++++++++++++----- src/ThrottlerInterface.php | 3 ++ src/WeightedRandomThrottler.php | 9 ++++-- src/WeightedRoundRobinThrottler.php | 32 ++++++++++++-------- 16 files changed, 127 insertions(+), 42 deletions(-) diff --git a/src/Cluster/Cluster.php b/src/Cluster/Cluster.php index 7699cdf..aeab8ba 100644 --- a/src/Cluster/Cluster.php +++ b/src/Cluster/Cluster.php @@ -16,6 +16,9 @@ public function __construct( ) { } + /** + * @param array $context + */ public function balance(ThrottlerInterface $throttler, array $context = []): NodeInterface { return $throttler->pick($this->collection, array_merge($context, [ diff --git a/src/Cluster/ClusterInterface.php b/src/Cluster/ClusterInterface.php index 2141b40..b3fc06e 100644 --- a/src/Cluster/ClusterInterface.php +++ b/src/Cluster/ClusterInterface.php @@ -9,5 +9,8 @@ interface ClusterInterface { + /** + * @param array $context + */ public function balance(ThrottlerInterface $throttler, array $context = []): NodeInterface; } diff --git a/src/Cluster/ClusterPool.php b/src/Cluster/ClusterPool.php index f9c243b..315af84 100644 --- a/src/Cluster/ClusterPool.php +++ b/src/Cluster/ClusterPool.php @@ -27,7 +27,7 @@ public function __construct(ClusterSet ...$clusterSets) foreach ($clusterSet->clusterNames as $clusterName) { if (isset($this->clusterNames[$clusterName])) { - throw new \UnexpectedValueException(sprintf('Cluster "%s" has already been added.', $clusterName)); + throw new \UnexpectedValueException(sprintf('The cluster "%s" has already been added.', $clusterName)); // @codeCoverageIgnore } $this->clusterNames[$clusterName] = $id; @@ -38,15 +38,15 @@ public function __construct(ClusterSet ...$clusterSets) public function pick(CollectionInterface $collection, array $context = []): NodeInterface { if (!isset($context['cluster'])) { - throw new \RuntimeException('The parameter "cluster" is required.'); + throw new \RuntimeException('Required parameter "cluster" is missing.'); // @codeCoverageIgnore } if (!\is_string($context['cluster'])) { - throw new \RuntimeException('The parameter "cluster" must be as a string.'); + throw new \RuntimeException('The parameter "cluster" must be as a string.'); // @codeCoverageIgnore } if (!isset($this->clusterNames[$context['cluster']])) { - throw new \RuntimeException(sprintf('The cluster "%s" is undefined.', $context['cluster'])); + throw new \RuntimeException(sprintf('The cluster "%s" is undefined.', $context['cluster'])); // @codeCoverageIgnore } $throttler = $this->throttlers[$this->clusterNames[$context['cluster']]]; diff --git a/src/Cluster/ClusterSet.php b/src/Cluster/ClusterSet.php index b5197f8..2873d8a 100644 --- a/src/Cluster/ClusterSet.php +++ b/src/Cluster/ClusterSet.php @@ -6,6 +6,9 @@ use Orangesoft\Throttler\ThrottlerInterface; +/** + * @codeCoverageIgnore + */ final class ClusterSet { /** diff --git a/src/Collection/CollectionInterface.php b/src/Collection/CollectionInterface.php index 2dfba96..095f40f 100644 --- a/src/Collection/CollectionInterface.php +++ b/src/Collection/CollectionInterface.php @@ -4,6 +4,10 @@ namespace Orangesoft\Throttler\Collection; +/** + * @template T of NodeInterface + * @template-extends \IteratorAggregate + */ interface CollectionInterface extends \Countable, \IteratorAggregate { public function add(NodeInterface $node): self; @@ -14,12 +18,15 @@ public function has(NodeInterface $node): bool; public function remove(NodeInterface $node): self; + /** + * @param callable(T, T): int $callback + */ public function sort(callable $callback): self; public function isEmpty(): bool; /** - * @return NodeInterface[] + * @return array */ public function toArray(): array; } diff --git a/src/Collection/InMemoryCollection.php b/src/Collection/InMemoryCollection.php index 5546ca7..c57f9ae 100644 --- a/src/Collection/InMemoryCollection.php +++ b/src/Collection/InMemoryCollection.php @@ -4,6 +4,9 @@ namespace Orangesoft\Throttler\Collection; +/** + * @template T of NodeInterface + */ final class InMemoryCollection implements CollectionInterface { /** @@ -72,9 +75,14 @@ public function remove(NodeInterface $node): self 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); @@ -82,7 +90,7 @@ public function sort(callable $callback): self public function isEmpty(): bool { - return 0 == $this->count(); + return 0 === $this->count(); } public function count(): int @@ -91,14 +99,18 @@ public function count(): int } /** - * @return NodeInterface[] + * @return array */ public function toArray(): array { return $this->nodes; } - public function getIterator(): \Traversable + /** + * @psalm-suppress ImplementedReturnTypeMismatch + * @return \ArrayIterator + */ + public function getIterator(): \ArrayIterator { return new \ArrayIterator($this->nodes); } diff --git a/src/Counter/CounterInterface.php b/src/Counter/CounterInterface.php index 62c3705..2b85d40 100644 --- a/src/Counter/CounterInterface.php +++ b/src/Counter/CounterInterface.php @@ -6,5 +6,5 @@ interface CounterInterface { - public function next(string $name = 'default', int $start = 0): int; + public function next(string $name = 'default'): int; } diff --git a/src/Counter/InMemoryCounter.php b/src/Counter/InMemoryCounter.php index 289a6e9..2ebdcdc 100644 --- a/src/Counter/InMemoryCounter.php +++ b/src/Counter/InMemoryCounter.php @@ -11,10 +11,15 @@ final class InMemoryCounter implements CounterInterface */ private array $counter = []; - public function next(string $name = 'default', int $start = 0): int + public function __construct( + private int $start = 0, + ) { + } + + public function next(string $name = 'default'): int { if (!isset($this->counter[$name])) { - $this->counter[$name] = $start; + $this->counter[$name] = $this->start; } $next = $this->counter[$name]; diff --git a/src/FrequencyRandomThrottler.php b/src/FrequencyRandomThrottler.php index 07dadf1..6e61958 100644 --- a/src/FrequencyRandomThrottler.php +++ b/src/FrequencyRandomThrottler.php @@ -15,10 +15,13 @@ public function __construct( ) { } + /** + * @param array $context + */ public function pick(CollectionInterface $collection, array $context = []): NodeInterface { if ($collection->isEmpty()) { - throw new \RuntimeException('Collection of nodes mustn\'t be empty.'); + throw new \RuntimeException('Collection of nodes mustn\'t be empty.'); // @codeCoverageIgnore } $total = \count($collection); diff --git a/src/MultipleThrottler.php b/src/MultipleThrottler.php index 427d8f6..28bf7f8 100644 --- a/src/MultipleThrottler.php +++ b/src/MultipleThrottler.php @@ -9,6 +9,9 @@ final class MultipleThrottler implements ThrottlerInterface { + /** + * @var array + */ private array $throttlers = []; public function __construct(ThrottlerInterface ...$throttlers) @@ -18,21 +21,23 @@ public function __construct(ThrottlerInterface ...$throttlers) } } + /** + * @param array $context + */ public function pick(CollectionInterface $collection, array $context = []): NodeInterface { - if (!isset($context['throttler'])) { - throw new \RuntimeException('Required parameter "throttler" is missing.'); + 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('Throttler "%s" is undefined.', $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('Throttler must be a class that exists and implements "%s" interface, "%s" given.', ThrottlerInterface::class, get_debug_type($context['throttler']))); + 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 } - /** @var ThrottlerInterface $throttler */ $throttler = $this->throttlers[$context['throttler']]; return $throttler->pick($collection, $context); diff --git a/src/RandomThrottler.php b/src/RandomThrottler.php index ee593fe..5c1bc49 100644 --- a/src/RandomThrottler.php +++ b/src/RandomThrottler.php @@ -9,10 +9,13 @@ final class RandomThrottler implements ThrottlerInterface { + /** + * @param array $context + */ public function pick(CollectionInterface $collection, array $context = []): NodeInterface { if ($collection->isEmpty()) { - throw new \RuntimeException('Collection of nodes mustn\'t be empty.'); + throw new \RuntimeException('Collection of nodes mustn\'t be empty.'); // @codeCoverageIgnore } $key = mt_rand(0, \count($collection) - 1); diff --git a/src/RoundRobinThrottler.php b/src/RoundRobinThrottler.php index b270ed9..fbd734a 100644 --- a/src/RoundRobinThrottler.php +++ b/src/RoundRobinThrottler.php @@ -15,10 +15,17 @@ public function __construct( ) { } + /** + * @param array $context + */ public function pick(CollectionInterface $collection, array $context = []): NodeInterface { if ($collection->isEmpty()) { - throw new \RuntimeException('Collection of nodes mustn\'t be empty.'); + 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 string, %s given.', get_debug_type($context['counter']))); // @codeCoverageIgnore } $counter = $context['counter'] ?? spl_object_hash($collection); diff --git a/src/SmoothWeightedRoundRobinThrottler.php b/src/SmoothWeightedRoundRobinThrottler.php index 5d7feb4..4344cb2 100644 --- a/src/SmoothWeightedRoundRobinThrottler.php +++ b/src/SmoothWeightedRoundRobinThrottler.php @@ -18,28 +18,48 @@ final class SmoothWeightedRoundRobinThrottler implements ThrottlerInterface */ 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.'); + 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 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.'); + if (0 === $node->getWeight()) { + throw new \RuntimeException('All nodes in the collection must be weighted.'); // @codeCoverageIgnore } - $this->weights[$counter][$key] = $this->currentWeights[$counter][$key] = $node->getWeight(); + $this->weights[$counter][$key] = $node->getWeight(); + $this->currentWeights[$counter][$key] = $node->getWeight(); } } - uasort($this->currentWeights[$counter], static fn (int $a, int $b): int => $a <=> $b); - $sumWeights = array_sum($this->weights[$counter]); - $maxCurrentWeightKey = array_key_last($this->currentWeights[$counter]); - $this->currentWeights[$counter][$maxCurrentWeightKey] -= $sumWeights; + 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; diff --git a/src/ThrottlerInterface.php b/src/ThrottlerInterface.php index 0cd3393..4effacc 100644 --- a/src/ThrottlerInterface.php +++ b/src/ThrottlerInterface.php @@ -9,5 +9,8 @@ interface ThrottlerInterface { + /** + * @param array $context + */ public function pick(CollectionInterface $collection, array $context = []): NodeInterface; } diff --git a/src/WeightedRandomThrottler.php b/src/WeightedRandomThrottler.php index e23c2da..5a91adc 100644 --- a/src/WeightedRandomThrottler.php +++ b/src/WeightedRandomThrottler.php @@ -9,10 +9,13 @@ final class WeightedRandomThrottler implements ThrottlerInterface { + /** + * @param array $context + */ public function pick(CollectionInterface $collection, array $context = []): NodeInterface { if ($collection->isEmpty()) { - throw new \RuntimeException('Collection of nodes mustn\'t be empty.'); + throw new \RuntimeException('Collection of nodes mustn\'t be empty.'); // @codeCoverageIgnore } $currentWeight = 0; @@ -21,7 +24,7 @@ public function pick(CollectionInterface $collection, array $context = []): Node foreach ($collection as $node) { if (0 == $node->getWeight()) { - throw new \RuntimeException('All nodes in the collection must be weighted.'); + throw new \RuntimeException('All nodes in the collection must be weighted.'); // @codeCoverageIgnore } $currentWeight += $node->getWeight(); @@ -31,6 +34,6 @@ public function pick(CollectionInterface $collection, array $context = []): Node } } - throw new \RuntimeException('You never will catch this exception.'); + throw new \RuntimeException('You never will catch this exception.'); // @codeCoverageIgnore } } diff --git a/src/WeightedRoundRobinThrottler.php b/src/WeightedRoundRobinThrottler.php index eba560f..570d00b 100644 --- a/src/WeightedRoundRobinThrottler.php +++ b/src/WeightedRoundRobinThrottler.php @@ -10,45 +10,53 @@ final class WeightedRoundRobinThrottler implements ThrottlerInterface { + private int $gcdWeight = 0; + private int $maxWeight = 0; + private int $currentWeight = 0; + 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.'); + 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 string, %s given.', get_debug_type($context['counter']))); // @codeCoverageIgnore } - $gcdWeight = 0; - $maxWeight = 0; - $currentWeight = 0; + $counter = $context['counter'] ?? spl_object_hash($collection); foreach ($collection as $node) { if (0 == $node->getWeight()) { - throw new \RuntimeException('All nodes in the collection must be weighted.'); + throw new \RuntimeException('All nodes in the collection must be weighted.'); // @codeCoverageIgnore } - $gcdWeight = gcd($gcdWeight, $node->getWeight()); - $maxWeight = max($maxWeight, $node->getWeight()); + $this->gcdWeight = gcd($this->gcdWeight, $node->getWeight()); + $this->maxWeight = max($this->maxWeight, $node->getWeight()); } while (true) { - $counter = $context['counter'] ?? spl_object_hash($collection); $key = $this->counter->next($counter) % \count($collection); if (0 == $key) { - $currentWeight -= $gcdWeight; + $this->currentWeight -= $this->gcdWeight; - if (0 >= $currentWeight) { - $currentWeight = $maxWeight; + if (0 >= $this->currentWeight) { + $this->currentWeight = $this->maxWeight; } } $node = $collection->get($key); - if ($node->getWeight() >= $currentWeight) { + if ($node->getWeight() >= $this->currentWeight) { return $node; } } From 6244058fe60cee4c5c7e862823bac0357fd45fb7 Mon Sep 17 00:00:00 2001 From: Aleksandr Denisyuk Date: Wed, 12 Jun 2024 16:03:56 +0300 Subject: [PATCH 05/20] Add tests --- tests/Cluster/ClusterTest.php | 49 ++++ tests/Collection/InMemoryCollectionTest.php | 223 ++++++++++++++++++ tests/Collection/NodeTest.php | 26 ++ tests/Counter/InMemoryCounterTest.php | 64 +++++ tests/FrequencyRandomThrottlerTest.php | 43 ++++ tests/MultipleThrottlerTest.php | 48 ++++ tests/RandomThrottlerTest.php | 38 +++ tests/RoundRobinThrottlerTest.php | 41 ++++ .../SmoothWeightedRoundRobinThrottlerTest.php | 41 ++++ tests/WeightedRandomThrottlerTest.php | 40 ++++ tests/WeightedRoundRobinThrottlerTest.php | 42 ++++ 11 files changed, 655 insertions(+) create mode 100644 tests/Cluster/ClusterTest.php create mode 100644 tests/Collection/InMemoryCollectionTest.php create mode 100644 tests/Collection/NodeTest.php create mode 100644 tests/Counter/InMemoryCounterTest.php create mode 100644 tests/FrequencyRandomThrottlerTest.php create mode 100644 tests/MultipleThrottlerTest.php create mode 100644 tests/RandomThrottlerTest.php create mode 100644 tests/RoundRobinThrottlerTest.php create mode 100644 tests/SmoothWeightedRoundRobinThrottlerTest.php create mode 100644 tests/WeightedRandomThrottlerTest.php create mode 100644 tests/WeightedRoundRobinThrottlerTest.php 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/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/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..af391a5 --- /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/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)); + } +} From bffff48fc8f0276c1e411d88ecb1255b9e203e7b Mon Sep 17 00:00:00 2001 From: Aleksandr Denisyuk Date: Wed, 19 Jun 2024 16:07:08 +0300 Subject: [PATCH 06/20] Update frequency random throttler --- src/FrequencyRandomThrottler.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/FrequencyRandomThrottler.php b/src/FrequencyRandomThrottler.php index 6e61958..9cea1df 100644 --- a/src/FrequencyRandomThrottler.php +++ b/src/FrequencyRandomThrottler.php @@ -6,6 +6,7 @@ use Orangesoft\Throttler\Collection\CollectionInterface; use Orangesoft\Throttler\Collection\NodeInterface; +use Orangesoft\Throttler\Collection\Sort\Desc; final class FrequencyRandomThrottler implements ThrottlerInterface { @@ -24,12 +25,13 @@ public function pick(CollectionInterface $collection, array $context = []): Node throw new \RuntimeException('Collection of nodes mustn\'t be empty.'); // @codeCoverageIgnore } - $total = \count($collection); + $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 $collection->get($key - 1); + return $sorted->get($key - 1); } } From ffe0cad8eb4240ceb1758cd753fa5d11f0a7ff41 Mon Sep 17 00:00:00 2001 From: Aleksandr Denisyuk Date: Wed, 19 Jun 2024 16:07:27 +0300 Subject: [PATCH 07/20] Update README.md --- README.md | 60 +++++++++++++++++++++++++++---------------------------- 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 02e9ea6..4a94df6 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ This package requires PHP 8.1 or later. ## Quick usage -Configure `Orangesoft\Throttler\WeightedRoundRobinThrottler::class` as below and set weight for each node as the second argument in constructor if you are using weighted strategies: +Configure `Orangesoft\Throttler\WeightedRoundRobinThrottler::class` as below and set weight for each node if you are using weighted strategy: ```php pick($collection); @@ -51,29 +48,31 @@ while (true) { } ``` -As a result, the strategy will go through all the nodes and return the appropriate one like below: +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 -+-------------+ -| 192.168.0.1 | -| 192.168.0.1 | -| 192.168.0.1 | -| 192.168.0.1 | -| 192.168.0.1 | -| 192.168.0.2 | -| 192.168.0.3 | -| etc. | -+-------------+ ++---------+-------------+ +| 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 load balancing strategies are available: +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) +- [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 @@ -96,13 +95,12 @@ The report is based on measuring the speed. Check `best` column to find out whic ## Documentation -- [How it works](docs/index.md##how-it-works) -- [Available strategies](docs/index.md##available-strategies) -- [Keep states](docs/index.md##keep-states) - - [Counting](docs/index.md##counting) - - [Serialization](docs/index.md##serialization) -- [Choice from multiple](docs/index.md##choice-from-multiple) -- [Balance cluster](docs/index.md##balance-cluster) -- [Production example](docs/index.md##production-example) +- [Available throttlers](./docs/index.md#available-throttlers) +- [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 [Load Balancing](https://samwho.dev/load-balancing/). +Read more about load balancing on [Sam Rose's blog](https://samwho.dev/load-balancing/). From 10bb4b3e5f480ac4d4a485240a64db8bbd19f333 Mon Sep 17 00:00:00 2001 From: Aleksandr Denisyuk Date: Wed, 19 Jun 2024 16:07:58 +0300 Subject: [PATCH 08/20] Redesign docs --- docs/index.md | 566 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 498 insertions(+), 68 deletions(-) diff --git a/docs/index.md b/docs/index.md index a7191aa..4f31cdb 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,102 +1,497 @@ # Documentation -- [How it works](#how-it-works) -- [Available strategies](#available-strategies) +- [Available throttlers](#available-throttlers) + - [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) - - [Counting](#counting) - - [Serialization](#serialization) -- [Choice from multiple](#choice-from-multiple) + - [Use counter](#use-counter) + - [Use serialization](#use-serialization) +- [Custom counter](#custom-counter) +- [Custom strategy](#custom-strategy) +- [Multiple throttler](#multiple-throttler) - [Balance cluster](#balance-cluster) -- [Production example](#production-example) +- [Guzzle middleware](#guzzle-middleware) -## How it works +## Available throttlers + +The following throttlers are available: + +### Random [...] -## Available strategies +```php +pick($collection); + + // ... +} +``` [...] -- [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) +```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% | +| 3 | 192.168.0.4 | 25.0% | +| n | etc. | | ++---------+-------------+--------+ +``` -## Keep states +[...] + +### Weighted random + +[...] + +```php +pick($collection); + + // ... +} +``` [...] ```text -+--------------------------+---------------+ -| Throttler | Method | -+--------------------------+---------------+ -| Random | [x] | -| WeightedRandom | [x] | -| FrequencyRandom | [x] | -| RoundRobin | counting | -| WeightedRoundRobin | counting | -| SmoothWeightedRoundRobin | serialization | -+--------------------------+---------------+ ++---------+-------------+--------+ +| request | node | chance | ++---------+-------------+--------+ +| 1 | 192.168.0.1 | 62.5% | +| 2 | 192.168.0.2 | 12.5% | +| 3 | 192.168.0.3 | 12.5% | +| 3 | 192.168.0.4 | 12.5% | +| n | etc. | | ++---------+-------------+--------+ ``` [...] -### Counting +### Frequency random + +[...] + +```php +pick($collection); + + // ... +} +``` [...] ```text -composer require predis/predis ++----------+--------------+--------+ +| 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 + +[...] + ```php client->exists($name)) { - $this->client->set($name, $start - 1); - } +$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'), +]); - return $this->client->incr($name); - } +while (true) { + /** @var NodeInterface $node */ + $node = $throttler->pick($collection); + + // ... } ``` [...] +```text ++---------+-------------+ +| request | node | ++---------+-------------+ +| 1 | 192.168.0.1 | +| 2 | 192.168.0.2 | +| 3 | 192.168.0.3 | +| 3 | 192.168.0.4 | +| n | etc. | ++---------+-------------+ +``` + +[...] + +### Weighted round-robin + +[...] + ```php -/** @var Predis\Client $client */ -$client = new Client('tcp://127.0.0.1:6379'); +pick($collection); + + // ... +} +``` + +[...] + +```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. | ++---------+-------------+ +``` + +[...] + +### Smooth weighted round-robin + +[...] + +```php +pick($collection); + + // ... +} +``` + +[...] + +```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 | +| 8 | 192.168.0.4 | +| n | etc. | ++---------+-------------+ +``` + +[...] + +## Keep states + +[...] + +```text ++-----------------------------+---------------+ +| Strategy | Method | ++-----------------------------+---------------+ +| Random | [x] | +| Weighted random | [x] | +| Frequency random | [x] | +| Round-robin | counter | +| Weighted round-robin | counter | +| Smooth weighted round-robin | serialization | ++-----------------------------+---------------+ +``` + +[...] + +### Use counter + +[...] + +```php +pick($collection, [ + 'counter' => 'other', + ]); + + // ... + + $counter++; +} +``` + +[...] + +```php +pick($collection); + + // ... +} + +/** @var string $serialized */ +$serialized = serialize($throttler); +``` + +[...] + +```php +/** @var SmoothWeightedRoundRobinThrottler $throttler */ +$throttler = unserialize($serialized); + +while (true) { + /** @var NodeInterface $node */ + $node = $throttler->pick($collection); + + // ... +} +``` [...] +## Custom counter +[...] -## Choice from multiple +```php + $context + */ + public function pick(CollectionInterface $collection, array $context = []) : NodeInterface + { + if ($collection->isEmpty()) { + throw new \RuntimeException('Collection of nodes mustn\'t be empty.'); + } + + // ... + } +}; +``` + +[...] + +## Multiple throttler [...] @@ -120,6 +515,7 @@ $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'), ]); ``` @@ -152,17 +548,18 @@ use Orangesoft\Throttler\Throttler\RandomThrottler; use Orangesoft\Throttler\Throttler\RoundRobinThrottler; $pool = new ClusterPool( - new ClusterSet(new RoundRobinThrottler(new InMemoryCounter()), ['cluster1']), - new ClusterSet(new RandomThrottler(), ['cluster2', 'cluster3']), + new ClusterSet(new RoundRobinThrottler(new InMemoryCounter()), ['a']), + new ClusterSet(new RandomThrottler(), ['b', 'c']), ); $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'), ]); -$cluster = new Cluster('cluster1', $collection); +$cluster = new Cluster('a', $collection); ``` [...] @@ -174,7 +571,7 @@ $node = $cluster->balance($pool); [...] -## Production example +## Guzzle middleware [...] @@ -182,7 +579,8 @@ $node = $cluster->balance($pool); composer require \ && orangesoft/throttler \ && guzzlehttp/guzzle \ - && psr/http-message + && psr/http-message \ + && predis/predis ``` [...] @@ -205,13 +603,11 @@ use Psr\Http\Message\ResponseInterface; final class ProxyMiddleware { /** - * @param ThrottlerInterface $throttler - * @param CollectionInterface $collection - * @param array $context + * @param array $context */ public function __construct( - private readonly ThrottlerInterface $throttler, - private readonly CollectionInterface $collection, + private ThrottlerInterface $throttler, + private CollectionInterface $collection, private array $context = [], ) { } @@ -221,6 +617,7 @@ final class ProxyMiddleware 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); @@ -232,14 +629,44 @@ final class ProxyMiddleware [...] ```php +client->exists($name)) { + $this->client->set($name, -1); + } + + return $this->client->incr($name); + } +} +``` + +[...] + +```php +/** @var Predis\Client $client */ +$client = new Client('tcp://127.0.0.1:6379'); + $throttler = new WeightedRoundRobinThrottler( - new InMemoryCounter(), + new RedisCounter($client), ); $collection = new InMemoryCollection([ new Node('user:pass@192.168.0.1', 5), new Node('user:pass@192.168.0.2', 1), new Node('user:pass@192.168.0.3', 1), + new Node('user:pass@192.168.0.4', 1), ]); $stack = HandlerStack::create(); @@ -261,16 +688,19 @@ while (true) { [...] ```text -+-------------+ -| 192.168.0.1 | -| 192.168.0.1 | -| 192.168.0.1 | -| 192.168.0.1 | -| 192.168.0.1 | -| 192.168.0.2 | -| 192.168.0.3 | -| etc. | -+-------------+ ++---------+-----------------------+ +| 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. | ++---------+-----------------------+ ``` [...] From 1107999691222453f92f05f1c2eda30dc54ab6dc Mon Sep 17 00:00:00 2001 From: Aleksandr Denisyuk Date: Fri, 21 Jun 2024 16:39:55 +0300 Subject: [PATCH 09/20] Update frequency random --- src/FrequencyRandomThrottler.php | 2 +- tests/FrequencyRandomThrottlerTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/FrequencyRandomThrottler.php b/src/FrequencyRandomThrottler.php index 9cea1df..c98185a 100644 --- a/src/FrequencyRandomThrottler.php +++ b/src/FrequencyRandomThrottler.php @@ -11,8 +11,8 @@ final class FrequencyRandomThrottler implements ThrottlerInterface { public function __construct( - private float $frequency = 0.8, private float $threshold = 0.2, + private float $frequency = 0.8, ) { } diff --git a/tests/FrequencyRandomThrottlerTest.php b/tests/FrequencyRandomThrottlerTest.php index af391a5..466b61c 100644 --- a/tests/FrequencyRandomThrottlerTest.php +++ b/tests/FrequencyRandomThrottlerTest.php @@ -14,8 +14,8 @@ final class FrequencyRandomThrottlerTest extends TestCase public function testFrequencyRandomAlgorithm(): void { $throttler = new FrequencyRandomThrottler( - frequency: 0.8, threshold: 0.2, + frequency: 0.8, ); $collection = new InMemoryCollection([ new Node('192.168.0.1'), From 5068168d625550fdcb561dad73a4a47c10269545 Mon Sep 17 00:00:00 2001 From: Aleksandr Denisyuk Date: Fri, 21 Jun 2024 16:40:03 +0300 Subject: [PATCH 10/20] Update docs --- docs/index.md | 64 +++++++++++++++++++++------------------------------ 1 file changed, 26 insertions(+), 38 deletions(-) diff --git a/docs/index.md b/docs/index.md index 4f31cdb..002f1bd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -22,7 +22,7 @@ The following throttlers are available: ### 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 below: ```php Date: Wed, 26 Jun 2024 11:07:02 +0300 Subject: [PATCH 11/20] Update strategies --- src/Cluster/ClusterPool.php | 2 +- src/RoundRobinThrottler.php | 2 +- src/SmoothWeightedRoundRobinThrottler.php | 2 +- src/WeightedRandomThrottler.php | 2 +- src/WeightedRoundRobinThrottler.php | 45 ++++++++++++++++------- 5 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/Cluster/ClusterPool.php b/src/Cluster/ClusterPool.php index 315af84..6b98c0f 100644 --- a/src/Cluster/ClusterPool.php +++ b/src/Cluster/ClusterPool.php @@ -42,7 +42,7 @@ public function pick(CollectionInterface $collection, array $context = []): Node } if (!\is_string($context['cluster'])) { - throw new \RuntimeException('The parameter "cluster" must be as a string.'); // @codeCoverageIgnore + 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']])) { diff --git a/src/RoundRobinThrottler.php b/src/RoundRobinThrottler.php index fbd734a..aa1e140 100644 --- a/src/RoundRobinThrottler.php +++ b/src/RoundRobinThrottler.php @@ -25,7 +25,7 @@ public function pick(CollectionInterface $collection, array $context = []): Node } if (isset($context['counter']) && !\is_string($context['counter'])) { - throw new \RuntimeException(sprintf('The parameter "counter" must be as string, %s given.', get_debug_type($context['counter']))); // @codeCoverageIgnore + 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); diff --git a/src/SmoothWeightedRoundRobinThrottler.php b/src/SmoothWeightedRoundRobinThrottler.php index 4344cb2..1c0d893 100644 --- a/src/SmoothWeightedRoundRobinThrottler.php +++ b/src/SmoothWeightedRoundRobinThrottler.php @@ -28,7 +28,7 @@ public function pick(CollectionInterface $collection, array $context = []): Node } if (isset($context['counter']) && !\is_string($context['counter'])) { - throw new \RuntimeException(sprintf('The parameter "counter" must be as string, %s given.', get_debug_type($context['counter']))); // @codeCoverageIgnore + 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); diff --git a/src/WeightedRandomThrottler.php b/src/WeightedRandomThrottler.php index 5a91adc..50ffdf7 100644 --- a/src/WeightedRandomThrottler.php +++ b/src/WeightedRandomThrottler.php @@ -23,7 +23,7 @@ public function pick(CollectionInterface $collection, array $context = []): Node $randomWeight = mt_rand(1, $sumWeight); foreach ($collection as $node) { - if (0 == $node->getWeight()) { + if (0 === $node->getWeight()) { throw new \RuntimeException('All nodes in the collection must be weighted.'); // @codeCoverageIgnore } diff --git a/src/WeightedRoundRobinThrottler.php b/src/WeightedRoundRobinThrottler.php index 570d00b..80a5465 100644 --- a/src/WeightedRoundRobinThrottler.php +++ b/src/WeightedRoundRobinThrottler.php @@ -10,9 +10,18 @@ final class WeightedRoundRobinThrottler implements ThrottlerInterface { - private int $gcdWeight = 0; - private int $maxWeight = 0; - private int $currentWeight = 0; + /** + * @var array + */ + private array $gcdWeight = []; + /** + * @var array + */ + private array $maxWeight = []; + /** + * @var array + */ + private array $currentWeight = []; public function __construct( private CounterInterface $counter, @@ -29,34 +38,42 @@ public function pick(CollectionInterface $collection, array $context = []): Node } if (isset($context['counter']) && !\is_string($context['counter'])) { - throw new \RuntimeException(sprintf('The parameter "counter" must be as string, %s given.', get_debug_type($context['counter']))); // @codeCoverageIgnore + 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); - foreach ($collection as $node) { - if (0 == $node->getWeight()) { - throw new \RuntimeException('All nodes in the collection must be weighted.'); // @codeCoverageIgnore - } + 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; + } - $this->gcdWeight = gcd($this->gcdWeight, $node->getWeight()); - $this->maxWeight = max($this->maxWeight, $node->getWeight()); + 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 -= $this->gcdWeight; + $this->currentWeight[$counter] -= $this->gcdWeight[$counter]; - if (0 >= $this->currentWeight) { - $this->currentWeight = $this->maxWeight; + if (0 >= $this->currentWeight[$counter]) { + $this->currentWeight[$counter] = $this->maxWeight[$counter]; } } $node = $collection->get($key); - if ($node->getWeight() >= $this->currentWeight) { + if ($node->getWeight() >= $this->currentWeight[$counter]) { return $node; } } From c05b877385b75f558c10424a523566d4098d5e82 Mon Sep 17 00:00:00 2001 From: Aleksandr Denisyuk Date: Wed, 26 Jun 2024 11:07:16 +0300 Subject: [PATCH 12/20] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4a94df6..e5e3e39 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ The report is based on measuring the speed. Check `best` column to find out whic ## Documentation -- [Available throttlers](./docs/index.md#available-throttlers) +- [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) From 02c8407cd44f25af456d191af36f3c8f9cbf83da Mon Sep 17 00:00:00 2001 From: Aleksandr Denisyuk Date: Wed, 26 Jun 2024 11:07:30 +0300 Subject: [PATCH 13/20] Update docs --- docs/index.md | 60 +++++++++++++++++++++++++-------------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/docs/index.md b/docs/index.md index 002f1bd..8a06d62 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ # Documentation -- [Available throttlers](#available-throttlers) +- [Available strategies](#available-strategies) - [Random](#random) - [Weighted random](#weighted-random) - [Frequency random](#frequency-random) @@ -8,7 +8,7 @@ - [Weighted round-robin](#weighted-round-robin) - [Smooth weighted round-robin](#smooth-weighted-round-robin) - [Keep states](#keep-states) - - [Use counter](#use-counter) + - [Use counting](#use-counting) - [Use serialization](#use-serialization) - [Custom counter](#custom-counter) - [Custom strategy](#custom-strategy) @@ -16,9 +16,9 @@ - [Balance cluster](#balance-cluster) - [Guzzle middleware](#guzzle-middleware) -## Available throttlers +## Available strategies -The following throttlers are available: +The following strategies are available: ### Random @@ -305,26 +305,26 @@ See a visualization of the smooth weighted round-robin strategy's output: ## 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 | +| strategy | method | +-----------------------------+---------------+ -| Random | [x] | -| Weighted random | [x] | -| Frequency random | [x] | -| Round-robin | counter | -| Weighted round-robin | counter | -| Smooth weighted round-robin | serialization | +| random | [x] | +| weighted random | [x] | +| frequency random | [x] | +| round-robin | counting | +| weighted round-robin | counting | +| smooth weighted round-robin | serialization | +-----------------------------+---------------+ ``` -[...] +This is especially useful when it's necessary to resume work precisely from where the previous process ended. -### Use counter +### Use counting -[...] +For round-robin and weighted round-robin strategies, `Orangesoft\Throttler\Counter\InMemoryCounter::class` is available, which stores the request count in memory: ```php pick($collection, [ - 'counter' => 'other', - ]); + $node = $throttler->pick($collection); // ... - + $counter++; } ``` -[...] +You can save the current request count in any storage and resume work from the last iteration as shown below: ```php Date: Wed, 26 Jun 2024 18:06:12 +0300 Subject: [PATCH 14/20] Update cluster --- src/Cluster/Cluster.php | 4 ++-- src/Cluster/ClusterInterface.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Cluster/Cluster.php b/src/Cluster/Cluster.php index aeab8ba..4da6fcd 100644 --- a/src/Cluster/Cluster.php +++ b/src/Cluster/Cluster.php @@ -19,9 +19,9 @@ public function __construct( /** * @param array $context */ - public function balance(ThrottlerInterface $throttler, array $context = []): NodeInterface + public function balance(ThrottlerInterface $pool, array $context = []): NodeInterface { - return $throttler->pick($this->collection, array_merge($context, [ + return $pool->pick($this->collection, array_merge($context, [ 'cluster' => $this->name, ])); } diff --git a/src/Cluster/ClusterInterface.php b/src/Cluster/ClusterInterface.php index b3fc06e..dd989e2 100644 --- a/src/Cluster/ClusterInterface.php +++ b/src/Cluster/ClusterInterface.php @@ -12,5 +12,5 @@ interface ClusterInterface /** * @param array $context */ - public function balance(ThrottlerInterface $throttler, array $context = []): NodeInterface; + public function balance(ThrottlerInterface $pool, array $context = []): NodeInterface; } From 6cd5dd79bcc928691fcfecc44cac7604201a89a0 Mon Sep 17 00:00:00 2001 From: Aleksandr Denisyuk Date: Wed, 26 Jun 2024 18:06:20 +0300 Subject: [PATCH 15/20] Update docs --- docs/index.md | 60 +++++++++++++++++++++++++++++---------------------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/docs/index.md b/docs/index.md index 8a06d62..be7f2dc 100644 --- a/docs/index.md +++ b/docs/index.md @@ -22,7 +22,7 @@ The following strategies are available: ### 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 below: +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, [ - 'throttler' => RoundRobinStrategy::class, -]); +$node = $throttler->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 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); +$node = $cluster->balance( + pool: $pool, + context: [ + 'counter' => InMemoryCounter::class, + ], +); ``` -[...] +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 From a0f84e57d46e14367919cc40dd8c0df356b5f5b6 Mon Sep 17 00:00:00 2001 From: Aleksandr Denisyuk Date: Wed, 26 Jun 2024 18:15:35 +0300 Subject: [PATCH 16/20] Update README.md --- README.md | 2 +- docs/index.md | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e5e3e39..87664ba 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ while (true) { } ``` -As a result, the throttler will go through all the nodes and return the appropriate one according to the chosen strategy, as shown below: +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 +---------+-------------+ diff --git a/docs/index.md b/docs/index.md index be7f2dc..fe3a3de 100644 --- a/docs/index.md +++ b/docs/index.md @@ -324,7 +324,7 @@ This is especially useful when it's necessary to resume work precisely from wher ### Use counting -For round-robin and weighted round-robin strategies, `Orangesoft\Throttler\Counter\InMemoryCounter::class` is available, which stores the request count in memory: +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 Date: Thu, 27 Jun 2024 11:00:48 +0300 Subject: [PATCH 17/20] Update docs --- docs/index.md | 53 ++++++++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/docs/index.md b/docs/index.md index fe3a3de..83e4f90 100644 --- a/docs/index.md +++ b/docs/index.md @@ -560,7 +560,7 @@ From the example above, the cluster of nodes named `Mercury` will work according $node = $cluster->balance( pool: $pool, context: [ - 'counter' => InMemoryCounter::class, + 'counter' => 'Mercury', ], ); ``` @@ -569,7 +569,7 @@ Note that you can also pass an optional context parameter `counter` with the cou ## 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 \ @@ -579,20 +579,16 @@ composer require \ && 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 client->exists($name)) { - $this->client->set($name, -1); + if (!$this->redis->exists($name)) { + $this->redis->set($name, -1); } - return $this->client->incr($name); + return $this->redis->incr($name); } } ``` -[...] +Now it’s time to configure load balancer and connect proxy middleware to Guzzle: ```php -/** @var Predis\Client $client */ -$client = new Client('tcp://127.0.0.1:6379'); +push(new ProxyMiddleware($throttler, $collection)); -$client = new Client(['handler' => $stack]); +$guzzle = new GuzzleClient(['handler' => $stack]); ``` -[...] +We can use Guzzle as always: ```php while (true) { /** @var ResponseInterface $response */ - $response = $client->get('https://httpbin.org/ip'); + $response = $guzzle->get('https://httpbin.org/ip'); // ... } ``` -[...] +The result of the proxy balancing will be as follows: ```text +---------+-----------------------+ @@ -699,4 +704,4 @@ while (true) { +---------+-----------------------+ ``` -[...] +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. From 526fa43f029e0324dbaeb3f820362be3ae065afe Mon Sep 17 00:00:00 2001 From: Aleksandr Denisyuk Date: Thu, 27 Jun 2024 11:01:01 +0300 Subject: [PATCH 18/20] Update .gitattributes --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) 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 From f406d4fa4be3ab6be43ea61074c93c7abc8f0e3e Mon Sep 17 00:00:00 2001 From: Aleksandr Denisyuk Date: Thu, 27 Jun 2024 11:07:30 +0300 Subject: [PATCH 19/20] Update docs --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 83e4f90..fd3d02f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -309,7 +309,7 @@ Load balancing strategies can be of 2 types: *random-based* and *round-robin bas ```text +-----------------------------+---------------+ -| strategy | method | +| Strategy | Method | +-----------------------------+---------------+ | random | [x] | | weighted random | [x] | From 8808bce9e167b2e5208f7813bbb27997c5088c3f Mon Sep 17 00:00:00 2001 From: Aleksandr Denisyuk Date: Thu, 27 Jun 2024 16:23:51 +0300 Subject: [PATCH 20/20] Update docs --- docs/index.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/index.md b/docs/index.md index fd3d02f..4b628cf 100644 --- a/docs/index.md +++ b/docs/index.md @@ -54,10 +54,10 @@ See a visualization of the random strategy's output: +---------+-------------+--------+ | 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% | +| 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. | | +---------+-------------+--------+ ``` @@ -96,10 +96,10 @@ See a visualization of the weighted random strategy's output: +---------+-------------+--------+ | 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% | +| 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. | | +---------+-------------+--------+ ``` @@ -147,17 +147,17 @@ See a visualization of the frequency random strategy's output: +----------+--------------+--------+ | request | node | chance | +----------+--------------+--------+ -| 1 | 192.168.0.10 | 40.0% | -| 2 | 192.168.0.9 | 40.9% | +| 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% | +| 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. | | +----------+--------------+--------+ ```