forked from phacility/phabricator
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathPhabricatorClientLimit.php
291 lines (234 loc) · 7.08 KB
/
PhabricatorClientLimit.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
<?php
abstract class PhabricatorClientLimit {
private $limitKey;
private $clientKey;
private $limit;
final public function setLimitKey($limit_key) {
$this->limitKey = $limit_key;
return $this;
}
final public function getLimitKey() {
return $this->limitKey;
}
final public function setClientKey($client_key) {
$this->clientKey = $client_key;
return $this;
}
final public function getClientKey() {
return $this->clientKey;
}
final public function setLimit($limit) {
$this->limit = $limit;
return $this;
}
final public function getLimit() {
return $this->limit;
}
final public function didConnect() {
// NOTE: We can not use pht() here because this runs before libraries
// load.
if (!function_exists('apc_fetch') && !function_exists('apcu_fetch')) {
throw new Exception(
'You can not configure connection rate limits unless APC/APCu are '.
'available. Rate limits rely on APC/APCu to track clients and '.
'connections.');
}
if ($this->getClientKey() === null) {
throw new Exception(
'You must configure a client key when defining a rate limit.');
}
if ($this->getLimitKey() === null) {
throw new Exception(
'You must configure a limit key when defining a rate limit.');
}
if ($this->getLimit() === null) {
throw new Exception(
'You must configure a limit when defining a rate limit.');
}
$points = $this->getConnectScore();
if ($points) {
$this->addScore($points);
}
$score = $this->getScore();
if (!$this->shouldRejectConnection($score)) {
// Client has not hit the limit, so continue processing the request.
return null;
}
$penalty = $this->getPenaltyScore();
if ($penalty) {
$this->addScore($penalty);
$score += $penalty;
}
return $this->getRateLimitReason($score);
}
final public function didDisconnect(array $request_state) {
$score = $this->getDisconnectScore($request_state);
if ($score) {
$this->addScore($score);
}
}
/**
* Get the number of seconds for each rate bucket.
*
* For example, a value of 60 will create one-minute buckets.
*
* @return int Number of seconds per bucket.
*/
abstract protected function getBucketDuration();
/**
* Get the total number of rate limit buckets to retain.
*
* @return int Total number of rate limit buckets to retain.
*/
abstract protected function getBucketCount();
/**
* Get the score to add when a client connects.
*
* @return double Connection score.
*/
abstract protected function getConnectScore();
/**
* Get the number of penalty points to add when a client hits a rate limit.
*
* @return double Penalty score.
*/
abstract protected function getPenaltyScore();
/**
* Get the score to add when a client disconnects.
*
* @return double Connection score.
*/
abstract protected function getDisconnectScore(array $request_state);
/**
* Get a human-readable explanation of why the client is being rejected.
*
* @return string Brief rejection message.
*/
abstract protected function getRateLimitReason($score);
/**
* Determine whether to reject a connection.
*
* @return bool True to reject the connection.
*/
abstract protected function shouldRejectConnection($score);
/**
* Get the APC key for the smallest stored bucket.
*
* @return string APC key for the smallest stored bucket.
* @task ratelimit
*/
private function getMinimumBucketCacheKey() {
$limit_key = $this->getLimitKey();
return "limit:min:{$limit_key}";
}
/**
* Get the current bucket ID for storing rate limit scores.
*
* @return int The current bucket ID.
*/
private function getCurrentBucketID() {
return (int)(time() / $this->getBucketDuration());
}
/**
* Get the APC key for a given bucket.
*
* @param int Bucket to get the key for.
* @return string APC key for the bucket.
*/
private function getBucketCacheKey($bucket_id) {
$limit_key = $this->getLimitKey();
return "limit:bucket:{$limit_key}:{$bucket_id}";
}
/**
* Add points to the rate limit score for some client.
*
* @param string Some key which identifies the client making the request.
* @param float The cost for this request; more points pushes them toward
* the limit faster.
* @return this
*/
private function addScore($score) {
$is_apcu = (bool)function_exists('apcu_fetch');
$current = $this->getCurrentBucketID();
$bucket_key = $this->getBucketCacheKey($current);
// There's a bit of a race here, if a second process reads the bucket
// before this one writes it, but it's fine if we occasionally fail to
// record a client's score. If they're making requests fast enough to hit
// rate limiting, we'll get them soon enough.
if ($is_apcu) {
$bucket = apcu_fetch($bucket_key);
} else {
$bucket = apc_fetch($bucket_key);
}
if (!is_array($bucket)) {
$bucket = array();
}
$client_key = $this->getClientKey();
if (empty($bucket[$client_key])) {
$bucket[$client_key] = 0;
}
$bucket[$client_key] += $score;
if ($is_apcu) {
@apcu_store($bucket_key, $bucket);
} else {
@apc_store($bucket_key, $bucket);
}
return $this;
}
/**
* Get the current rate limit score for a given client.
*
* @return float The client's current score.
* @task ratelimit
*/
private function getScore() {
$is_apcu = (bool)function_exists('apcu_fetch');
// Identify the oldest bucket stored in APC.
$min_key = $this->getMinimumBucketCacheKey();
if ($is_apcu) {
$min = apcu_fetch($min_key);
} else {
$min = apc_fetch($min_key);
}
// If we don't have any buckets stored yet, store the current bucket as
// the oldest bucket.
$cur = $this->getCurrentBucketID();
if (!$min) {
if ($is_apcu) {
@apcu_store($min_key, $cur);
} else {
@apc_store($min_key, $cur);
}
$min = $cur;
}
// Destroy any buckets that are older than the minimum bucket we're keeping
// track of. Under load this normally shouldn't do anything, but will clean
// up an old bucket once per minute.
$count = $this->getBucketCount();
for ($cursor = $min; $cursor < ($cur - $count); $cursor++) {
$bucket_key = $this->getBucketCacheKey($cursor);
if ($is_apcu) {
apcu_delete($bucket_key);
@apcu_store($min_key, $cursor + 1);
} else {
apc_delete($bucket_key);
@apc_store($min_key, $cursor + 1);
}
}
$client_key = $this->getClientKey();
// Now, sum up the client's scores in all of the active buckets.
$score = 0;
for (; $cursor <= $cur; $cursor++) {
$bucket_key = $this->getBucketCacheKey($cursor);
if ($is_apcu) {
$bucket = apcu_fetch($bucket_key);
} else {
$bucket = apc_fetch($bucket_key);
}
if (isset($bucket[$client_key])) {
$score += $bucket[$client_key];
}
}
return $score;
}
}