Passed
Pull Request — master (#204)
by Alexander
01:57
created

Counter::calculateArrivalTime()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Web\RateLimiter;
6
7
use Psr\SimpleCache\CacheInterface;
8
9
/**
10
 * Counter implements generic сell rate limit algorithm (GCRA) that ensures that after reaching the limit futher
11
 * requests are distributed equally.
12
 *
13
 * @link https://en.wikipedia.org/wiki/Generic_cell_rate_algorithm
14
 */
15
final class Counter implements CounterInterface
16
{
17
    public const ID_PREFIX = 'rate-limiter-';
18
19
    private const MILLISECONDS_PER_SECOND = 1000;
20
21
    private int $period;
22
23
    private int $limit;
24
25
    private float $emissionInterval;
26
27
    private ?string $id = null;
28
29
    private CacheInterface $storage;
30
31
    private int $arrivalTime;
32
33
    public function __construct(int $limit, int $period, CacheInterface $storage)
34
    {
35
        if ($limit < 1) {
36
            throw new \InvalidArgumentException('The limit must be a positive value.');
37
        }
38
39
        if ($period < 1) {
40
            throw new \InvalidArgumentException('The period must be a positive value.');
41
        }
42
43
        $this->limit = $limit;
44
        $this->period = $period * self::MILLISECONDS_PER_SECOND;
45
46
        $this->emissionInterval = (float)($this->period / $this->limit);
47
        $this->storage = $storage;
48
    }
49
50
    public function setId(string $id): void
51
    {
52
        $this->id = self::ID_PREFIX . $id;
53
    }
54
55
    public function incrementAndGetResult(): CounterStatistics
56
    {
57
        if ($this->id === null) {
58
            throw new \LogicException('The counter ID should be set');
59
        }
60
61
        $this->arrivalTime = $this->calculateArrivalTime();
62
        $theoreticalArrivalTime = $this->calculateTheoreticalArrivalTime($this->getStorageValue());
63
        $remaining = $this->calculateRemaining($theoreticalArrivalTime);
64
        $resetAfter = $this->calculateResetAfter($theoreticalArrivalTime);
65
66
        if ($remaining < 1) {
67
            $remaining = 0;
68
        } else {
69
            $this->setStorageValue($theoreticalArrivalTime);
70
        }
71
72
        return new CounterStatistics($this->limit, $remaining, $resetAfter);
73
    }
74
75
    private function calculateTheoreticalArrivalTime(float $theoreticalArrivalTime): float
76
    {
77
        return max($this->arrivalTime, $theoreticalArrivalTime) + $this->emissionInterval;
78
    }
79
80
    private function calculateRemaining(float $theoreticalArrivalTime): int
81
    {
82
        $allowAt = $theoreticalArrivalTime - $this->period;
83
84
        return (int)((floor($this->arrivalTime - $allowAt) / $this->emissionInterval) + 0.5);
85
    }
86
87
    private function getStorageValue(): float
88
    {
89
        return $this->storage->get($this->id, (float)$this->arrivalTime);
90
    }
91
92
    private function setStorageValue(float $theoreticalArrivalTime): void
93
    {
94
        $this->storage->set($this->id, $theoreticalArrivalTime);
95
    }
96
97
    private function calculateArrivalTime(): int
98
    {
99
        return time() * self::MILLISECONDS_PER_SECOND;
100
    }
101
102
    private function calculateResetAfter(float $theoreticalArrivalTime): int
103
    {
104
        return (int)($theoreticalArrivalTime - $this->arrivalTime);
105
    }
106
}
107