Passed
Pull Request — master (#14)
by Alexander
04:24 queued 02:15
created

Counter::setTtlInSeconds()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
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 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 4
        $lastIncrementTimeInMilliseconds = $this->currentTimeInMilliseconds();
99 4
        $theoreticalNextIncrementTime = $this->calculateTheoreticalNextIncrementTime(
100 4
            $lastIncrementTimeInMilliseconds,
101 4
            $this->getLastStoredTheoreticalNextIncrementTime($lastIncrementTimeInMilliseconds)
102
        );
103 4
        $remaining = $this->calculateRemaining($lastIncrementTimeInMilliseconds, $theoreticalNextIncrementTime);
104 4
        $resetAfter = $this->calculateResetAfter($theoreticalNextIncrementTime);
105
106 4
        if ($remaining >= 1) {
107 3
            $this->storeTheoreticalNextIncrementTime($theoreticalNextIncrementTime);
108
        }
109
110 4
        return new CounterState($this->limit, $remaining, $resetAfter);
111
    }
112
113
    /**
114
     * @return float Theoretical increment time that would be expected from equally spaced increments at exactly rate
115
     * limit. In GCRA it is known as TAT, theoretical arrival time.
116
     */
117 4
    private function calculateTheoreticalNextIncrementTime(
118
        int $lastIncrementTimeInMilliseconds,
119
        float $storedTheoreticalNextIncrementTime
120
    ): float {
121 4
        return max($lastIncrementTimeInMilliseconds, $storedTheoreticalNextIncrementTime) +
122 4
            $this->incrementIntervalInMilliseconds;
123
    }
124
125
    /**
126
     * @return int The number of remaining requests in the current time period.
127
     */
128 4
    private function calculateRemaining(int $lastIncrementTimeInMilliseconds, float $theoreticalNextIncrementTime): int
129
    {
130 4
        $incrementAllowedAt = $theoreticalNextIncrementTime - $this->periodInMilliseconds;
131
132
        return (int)(
133 4
            round($lastIncrementTimeInMilliseconds - $incrementAllowedAt) /
134 4
            $this->incrementIntervalInMilliseconds
135
        );
136
    }
137
138 4
    private function getLastStoredTheoreticalNextIncrementTime(int $lastIncrementTimeInMilliseconds): float
139
    {
140 4
        return (float)$this->storage->get($this->getCacheKey(), $lastIncrementTimeInMilliseconds);
141
    }
142
143 3
    private function storeTheoreticalNextIncrementTime(float $theoreticalNextIncrementTime): void
144
    {
145 3
        $this->storage->set($this->getCacheKey(), $theoreticalNextIncrementTime, $this->ttlInSeconds);
146 3
    }
147
148
    /**
149
     * @return int Timestamp to wait until the rate limit resets.
150
     */
151 4
    private function calculateResetAfter(float $theoreticalNextIncrementTime): int
152
    {
153 4
        return (int)($theoreticalNextIncrementTime / self::MILLISECONDS_PER_SECOND);
154
    }
155
156 4
    private function currentTimeInMilliseconds(): int
157
    {
158 4
        return (int)round(microtime(true) * self::MILLISECONDS_PER_SECOND);
159
    }
160
}
161