Passed
Push — master ( bcc437...f72709 )
by Alexander
01:35
created

Counter::setTtlInSeconds()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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