Passed
Push — master ( ed97dc...4444c0 )
by Alexander
02:45
created

Counter   A

Complexity

Total Complexity 16

Size/Duplication

Total Lines 145
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 44
c 2
b 0
f 0
dl 0
loc 145
ccs 48
cts 48
cp 1
rs 10
wmc 16

11 Methods

Rating   Name   Duplication   Size   Complexity  
A getCacheKey() 0 7 2
A __construct() 0 15 3
A setId() 0 3 1
A setTtlInSeconds() 0 3 1
A calculateTheoreticalNextIncrementTime() 0 6 1
A calculateResetAfter() 0 3 1
A calculateRemaining() 0 7 1
A getLastStoredTheoreticalNextIncrementTime() 0 3 1
A currentTimeInMilliseconds() 0 3 1
A storeTheoreticalNextIncrementTime() 0 3 1
A incrementAndGetState() 0 22 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\RateLimiter;
6
7
use InvalidArgumentException;
8
use LogicException;
9
use Psr\SimpleCache\CacheInterface;
10
11
/**
12
 * Counter implements generic cell rate limit algorithm (GCRA) that ensures that after reaching the limit further
13
 * increments are distributed equally.
14
 *
15
 * @link https://en.wikipedia.org/wiki/Generic_cell_rate_algorithm
16
 */
17
final class Counter implements CounterInterface
18
{
19
    private const DEFAULT_TTL = 86400;
20
21
    private const ID_PREFIX = 'rate-limiter-';
22
23
    private const MILLISECONDS_PER_SECOND = 1000;
24
25
    /**
26
     * @var int Period to apply limit to.
27
     */
28
    private int $periodInMilliseconds;
29
30
    private int $limit;
31
32
    /**
33
     * @var float Maximum interval before next increment.
34
     * In GCRA it is known as emission interval.
35
     */
36
    private float $incrementIntervalInMilliseconds;
37
38
    private ?string $id = null;
39
40
    private CacheInterface $storage;
41
42
    private int $ttlInSeconds = self::DEFAULT_TTL;
43
44
    /**
45
     * @param int $limit Maximum number of increments that could be performed before increments are limited.
46
     * @param int $periodInSeconds Period to apply limit to.
47
     */
48 8
    public function __construct(int $limit, int $periodInSeconds, CacheInterface $storage)
49
    {
50 8
        if ($limit < 1) {
51 1
            throw new InvalidArgumentException('The limit must be a positive value.');
52
        }
53
54 7
        if ($periodInSeconds < 1) {
55 1
            throw new InvalidArgumentException('The period must be a positive value.');
56
        }
57
58 6
        $this->limit = $limit;
59 6
        $this->periodInMilliseconds = $periodInSeconds * self::MILLISECONDS_PER_SECOND;
60 6
        $this->storage = $storage;
61
62 6
        $this->incrementIntervalInMilliseconds = (float)($this->periodInMilliseconds / $this->limit);
63 6
    }
64
65 4
    public function setId(string $id): void
66
    {
67 4
        $this->id = $id;
68 4
    }
69
70
    /**
71
     * @param int $secondsTTL Cache TTL that is used to store counter values.
72
     * Default is one day.
73
     * Note that period can not exceed TTL.
74
     */
75 1
    public function setTtlInSeconds(int $secondsTTL): void
76
    {
77 1
        $this->ttlInSeconds = $secondsTTL;
78 1
    }
79
80
    /**
81
     * @return string Cache key used to store the next increment time.
82
     */
83 5
    public function getCacheKey(): string
84
    {
85 5
        if ($this->id === null) {
86 1
            throw new LogicException('The counter ID should be set.');
87
        }
88
89 4
        return self::ID_PREFIX . $this->id;
90
    }
91
92 5
    public function incrementAndGetState(): CounterState
93
    {
94 5
        if ($this->id === null) {
95 1
            throw new LogicException('The counter ID should be set.');
96
        }
97
98
        // Last increment time.
99
        // In GCRA it's known as arrival time.
100 4
        $lastIncrementTimeInMilliseconds = $this->currentTimeInMilliseconds();
101
102 4
        $theoreticalNextIncrementTime = $this->calculateTheoreticalNextIncrementTime(
103 4
            $lastIncrementTimeInMilliseconds,
104 4
            $this->getLastStoredTheoreticalNextIncrementTime($lastIncrementTimeInMilliseconds)
105
        );
106 4
        $remaining = $this->calculateRemaining($lastIncrementTimeInMilliseconds, $theoreticalNextIncrementTime);
107 4
        $resetAfter = $this->calculateResetAfter($theoreticalNextIncrementTime);
108
109 4
        if ($remaining >= 1) {
110 3
            $this->storeTheoreticalNextIncrementTime($theoreticalNextIncrementTime);
111
        }
112
113 4
        return new CounterState($this->limit, $remaining, $resetAfter);
114
    }
115
116
    /**
117
     * @return float Theoretical increment time that would be expected from equally spaced increments at exactly rate
118
     * limit. In GCRA it is known as TAT, theoretical arrival time.
119
     */
120 4
    private function calculateTheoreticalNextIncrementTime(
121
        int $lastIncrementTimeInMilliseconds,
122
        float $storedTheoreticalNextIncrementTime
123
    ): float {
124 4
        return max($lastIncrementTimeInMilliseconds, $storedTheoreticalNextIncrementTime) +
125 4
            $this->incrementIntervalInMilliseconds;
126
    }
127
128
    /**
129
     * @return int The number of remaining requests in the current time period.
130
     */
131 4
    private function calculateRemaining(int $lastIncrementTimeInMilliseconds, float $theoreticalNextIncrementTime): int
132
    {
133 4
        $incrementAllowedAt = $theoreticalNextIncrementTime - $this->periodInMilliseconds;
134
135
        return (int)(
136 4
            round($lastIncrementTimeInMilliseconds - $incrementAllowedAt) /
137 4
            $this->incrementIntervalInMilliseconds
138
        );
139
    }
140
141 4
    private function getLastStoredTheoreticalNextIncrementTime(int $lastIncrementTimeInMilliseconds): float
142
    {
143 4
        return (float)$this->storage->get($this->getCacheKey(), $lastIncrementTimeInMilliseconds);
144
    }
145
146 3
    private function storeTheoreticalNextIncrementTime(float $theoreticalNextIncrementTime): void
147
    {
148 3
        $this->storage->set($this->getCacheKey(), $theoreticalNextIncrementTime, $this->ttlInSeconds);
149 3
    }
150
151
    /**
152
     * @return int Timestamp to wait until the rate limit resets.
153
     */
154 4
    private function calculateResetAfter(float $theoreticalNextIncrementTime): int
155
    {
156 4
        return (int)($theoreticalNextIncrementTime / self::MILLISECONDS_PER_SECOND);
157
    }
158
159 4
    private function currentTimeInMilliseconds(): int
160
    {
161 4
        return (int)round(microtime(true) * self::MILLISECONDS_PER_SECOND);
162
    }
163
}
164