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