Test Failed
Pull Request — master (#19)
by Alexander
01:57
created

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