Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
96.88% |
62 / 64 |
|
80.00% |
8 / 10 |
CRAP | |
0.00% |
0 / 1 |
WRStatsRateLimiter | |
96.88% |
62 / 64 |
|
80.00% |
8 / 10 |
19 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
2 | |||
createBatch | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
peek | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
peekBatch | |
96.15% |
25 / 26 |
|
0.00% |
0 / 1 |
6 | |||
tryIncr | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
tryIncrBatch | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
incr | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
incrBatch | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
setCurrentTime | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
resetCurrentTime | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace Wikimedia\WRStats; |
4 | |
5 | /** |
6 | * A rate limiter with a WRStats backend |
7 | * |
8 | * @since 1.39 |
9 | */ |
10 | class WRStatsRateLimiter { |
11 | /** @var StatsStore */ |
12 | private $store; |
13 | /** @var LimitCondition[] */ |
14 | private $conditions; |
15 | /** @var array */ |
16 | private $specs; |
17 | /** @var string|string[] */ |
18 | private $prefix; |
19 | /** @var float|int|null */ |
20 | private $now; |
21 | |
22 | /** Default number of time buckets per action */ |
23 | public const BUCKET_COUNT = 30; |
24 | |
25 | /** |
26 | * @internal Use WRStatsFactory::createRateLimiter instead |
27 | * @param StatsStore $store |
28 | * @param LimitCondition[] $conditions |
29 | * @param string|string[] $prefix |
30 | * @param array $options |
31 | */ |
32 | public function __construct( |
33 | StatsStore $store, |
34 | $conditions, |
35 | $prefix = 'WRLimit', |
36 | $options = [] |
37 | ) { |
38 | $this->store = $store; |
39 | $this->conditions = $conditions; |
40 | $this->prefix = $prefix; |
41 | $bucketCount = $options['bucketCount'] ?? self::BUCKET_COUNT; |
42 | |
43 | $specs = []; |
44 | foreach ( $conditions as $name => $condition ) { |
45 | $specs[$name] = [ |
46 | 'sequences' => [ [ |
47 | 'timeStep' => $condition->window / $bucketCount, |
48 | 'expiry' => $condition->window |
49 | ] ] |
50 | ]; |
51 | } |
52 | $this->specs = $specs; |
53 | } |
54 | |
55 | /** |
56 | * Create a batch object for rate limiting of multiple metrics. |
57 | * |
58 | * @param int $defaultAmount The amount to increment each metric by, if no |
59 | * amount is passed to localOp/globalOp |
60 | * @return LimitBatch |
61 | */ |
62 | public function createBatch( $defaultAmount = 1 ) { |
63 | return new LimitBatch( $this, $defaultAmount ); |
64 | } |
65 | |
66 | /** |
67 | * Check whether executing a single operation would exceed the defined limit, |
68 | * without incrementing the count. |
69 | * |
70 | * @param string $condName |
71 | * @param EntityKey|null $entityKey |
72 | * @param int $amount |
73 | * @return LimitOperationResult |
74 | */ |
75 | public function peek( |
76 | string $condName, |
77 | ?EntityKey $entityKey = null, |
78 | $amount = 1 |
79 | ): LimitOperationResult { |
80 | $actions = [ new LimitOperation( $condName, $entityKey, $amount ) ]; |
81 | $result = $this->peekBatch( $actions ); |
82 | return $result->getAllResults()[0]; |
83 | } |
84 | |
85 | /** |
86 | * Check whether executing a given set of increment operations would exceed |
87 | * any defined limit, without actually performing the increment. |
88 | * |
89 | * @param LimitOperation[] $operations |
90 | * @return LimitBatchResult |
91 | */ |
92 | public function peekBatch( array $operations ) { |
93 | $reader = new WRStatsReader( $this->store, $this->specs, $this->prefix ); |
94 | if ( $this->now !== null ) { |
95 | $reader->setCurrentTime( $this->now ); |
96 | } |
97 | |
98 | $rates = []; |
99 | $amounts = []; |
100 | foreach ( $operations as $operation ) { |
101 | $name = $operation->condName; |
102 | $cond = $this->conditions[$name] ?? null; |
103 | if ( $cond === null ) { |
104 | throw new WRStatsError( "Unrecognized metric \"$name\"" ); |
105 | } |
106 | if ( !isset( $rates[$name] ) ) { |
107 | $range = $reader->latest( $cond->window ); |
108 | $rates[$name] = $reader->getRate( $name, $operation->entityKey, $range ); |
109 | $amounts[$name] = 0; |
110 | } |
111 | $amounts[$name] += $operation->amount; |
112 | } |
113 | |
114 | $results = []; |
115 | foreach ( $operations as $i => $operation ) { |
116 | $name = $operation->condName; |
117 | $total = $rates[$name]->total(); |
118 | $cond = $this->conditions[$name]; |
119 | $results[$i] = new LimitOperationResult( |
120 | $cond, |
121 | $total, |
122 | $total + $amounts[$name] |
123 | ); |
124 | } |
125 | return new LimitBatchResult( $results ); |
126 | } |
127 | |
128 | /** |
129 | * Check if the limit would be exceeded by incrementing the specified |
130 | * metric. If not, increment it. |
131 | * |
132 | * @param string $condName |
133 | * @param EntityKey|null $entityKey |
134 | * @param int $amount |
135 | * @return LimitOperationResult |
136 | */ |
137 | public function tryIncr( |
138 | string $condName, |
139 | ?EntityKey $entityKey = null, |
140 | $amount = 1 |
141 | ): LimitOperationResult { |
142 | $actions = [ new LimitOperation( $condName, $entityKey, $amount ) ]; |
143 | $result = $this->tryIncrBatch( $actions ); |
144 | return $result->getAllResults()[0]; |
145 | } |
146 | |
147 | /** |
148 | * Check if the limit would be exceeded by execution of the given set of |
149 | * increment operations. If not, perform the increments. |
150 | * |
151 | * @param LimitOperation[] $operations |
152 | * @return LimitBatchResult |
153 | */ |
154 | public function tryIncrBatch( array $operations ) { |
155 | $result = $this->peekBatch( $operations ); |
156 | if ( $result->isAllowed() ) { |
157 | $this->incrBatch( $operations ); |
158 | } |
159 | return $result; |
160 | } |
161 | |
162 | /** |
163 | * Unconditionally increment a metric. |
164 | * |
165 | * @param string $condName |
166 | * @param EntityKey|null $entityKey |
167 | * @param int $amount |
168 | * @return void |
169 | */ |
170 | public function incr( |
171 | string $condName, |
172 | ?EntityKey $entityKey = null, |
173 | $amount = 1 |
174 | ) { |
175 | $actions = [ new LimitOperation( $condName, $entityKey, $amount ) ]; |
176 | $this->incrBatch( $actions ); |
177 | } |
178 | |
179 | /** |
180 | * Unconditionally increment a set of metrics. |
181 | * |
182 | * @param LimitOperation[] $operations |
183 | */ |
184 | public function incrBatch( array $operations ) { |
185 | $writer = new WRStatsWriter( $this->store, $this->specs, $this->prefix ); |
186 | if ( $this->now !== null ) { |
187 | $writer->setCurrentTime( $this->now ); |
188 | } |
189 | foreach ( $operations as $operation ) { |
190 | $writer->incr( |
191 | $operation->condName, |
192 | $operation->entityKey, |
193 | $operation->amount |
194 | ); |
195 | } |
196 | $writer->flush(); |
197 | } |
198 | |
199 | /** |
200 | * Set the current time. |
201 | * |
202 | * @param float|int $now |
203 | */ |
204 | public function setCurrentTime( $now ) { |
205 | $this->now = $now; |
206 | } |
207 | |
208 | /** |
209 | * Forget a time set with setCurrentTime(). Use the actual current time. |
210 | */ |
211 | public function resetCurrentTime() { |
212 | $this->now = null; |
213 | } |
214 | } |