Passed
Pull Request — master (#204)
by Alexander
02:03
created

Counter::storeTheoreticalNextIncrementTime()   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 1
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
 * increments 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
    /**
22
     * @var int period to apply limit to, in milliseconds
23
     */
24
    private int $period;
25
26
    private int $limit;
27
28
    /**
29
     * @var float maximum interval before next increment, in milliseconds
30
     * In GCRA it is known as emission interval.
31
     */
32
    private float $incrementInterval;
33
34
    private ?string $id = null;
35
36
    private CacheInterface $storage;
37
38
    /**
39
     * @var int last increment time
40
     * In GCRA it's known as arrival time
41
     */
42
    private int $lastIncrementTime;
43
44
    /**
45
     * @param int $limit maximum number of increments that could be performed before increments are limited
46
     * @param int $period period to apply limit to, in seconds
47
     * @param CacheInterface $storage
48
     */
49
    public function __construct(int $limit, int $period, CacheInterface $storage)
50
    {
51
        if ($limit < 1) {
52
            throw new \InvalidArgumentException('The limit must be a positive value.');
53
        }
54
55
        if ($period < 1) {
56
            throw new \InvalidArgumentException('The period must be a positive value.');
57
        }
58
59
        $this->limit = $limit;
60
        $this->period = $period * self::MILLISECONDS_PER_SECOND;
61
        $this->storage = $storage;
62
63
        $this->incrementInterval = (float)($this->period / $this->limit);
64
    }
65
66
    public function setId(string $id): void
67
    {
68
        $this->id = self::ID_PREFIX . $id;
69
    }
70
71
    public function incrementAndGetResult(): CounterStatistics
72
    {
73
        if ($this->id === null) {
74
            throw new \LogicException('The counter ID should be set');
75
        }
76
77
        $this->lastIncrementTime = time() * self::MILLISECONDS_PER_SECOND;
78
        $theoreticalNextIncrementTime = $this->calculateTheoreticalNextIncrementTime($this->getLastStoredTheoreticalNextIncrementTime());
79
        $remaining = $this->calculateRemaining($theoreticalNextIncrementTime);
80
        $resetAfter = $this->calculateResetAfter($theoreticalNextIncrementTime);
81
82
        if ($remaining >= 1) {
83
            $this->storeTheoreticalNextIncrementTime($theoreticalNextIncrementTime);
84
        }
85
86
        return new CounterStatistics($this->limit, $remaining, $resetAfter);
87
    }
88
89
    /**
90
     * @param float $storedTheoreticalNextIncrementTime
91
     * @return float theoretical increment time that would be expected from equally spaced increments at exactly rate limit
92
     * In GCRA it is known as TAT, theoretical arrival time.
93
     */
94
    private function calculateTheoreticalNextIncrementTime(float $storedTheoreticalNextIncrementTime): float
95
    {
96
        return max($this->lastIncrementTime, $storedTheoreticalNextIncrementTime) + $this->incrementInterval;
97
    }
98
99
    /**
100
     * @param float $theoreticalNextIncrementTime
101
     * @return int the number of remaining requests in the current time period
102
     */
103
    private function calculateRemaining(float $theoreticalNextIncrementTime): int
104
    {
105
        $incrementAllowedAt = $theoreticalNextIncrementTime - $this->period;
106
107
        return (int)((floor($this->lastIncrementTime - $incrementAllowedAt) / $this->incrementInterval) + 0.5);
108
    }
109
110
    private function getLastStoredTheoreticalNextIncrementTime(): float
111
    {
112
        return $this->storage->get($this->id, (float)$this->lastIncrementTime);
113
    }
114
115
    private function storeTheoreticalNextIncrementTime(float $theoreticalNextIncrementTime): void
116
    {
117
        $this->storage->set($this->id, $theoreticalNextIncrementTime);
118
    }
119
120
    /**
121
     * @param float $theoreticalNextIncrementTime
122
     * @return int milliseconds to wait until the rate limit resets
123
     */
124
    private function calculateResetAfter(float $theoreticalNextIncrementTime): int
125
    {
126
        return (int)($theoreticalNextIncrementTime - $this->lastIncrementTime);
127
    }
128
}
129