3 abstract class PhabricatorClientLimit
{
9 final public function setLimitKey($limit_key) {
10 $this->limitKey
= $limit_key;
14 final public function getLimitKey() {
15 return $this->limitKey
;
18 final public function setClientKey($client_key) {
19 $this->clientKey
= $client_key;
23 final public function getClientKey() {
24 return $this->clientKey
;
27 final public function setLimit($limit) {
28 $this->limit
= $limit;
32 final public function getLimit() {
36 final public function didConnect() {
37 // NOTE: We can not use pht() here because this runs before libraries
40 if (!function_exists('apc_fetch') && !function_exists('apcu_fetch')) {
42 'You can not configure connection rate limits unless APC/APCu are '.
43 'available. Rate limits rely on APC/APCu to track clients and '.
47 if ($this->getClientKey() === null) {
49 'You must configure a client key when defining a rate limit.');
52 if ($this->getLimitKey() === null) {
54 'You must configure a limit key when defining a rate limit.');
57 if ($this->getLimit() === null) {
59 'You must configure a limit when defining a rate limit.');
62 $points = $this->getConnectScore();
64 $this->addScore($points);
67 $score = $this->getScore();
68 if (!$this->shouldRejectConnection($score)) {
69 // Client has not hit the limit, so continue processing the request.
73 $penalty = $this->getPenaltyScore();
75 $this->addScore($penalty);
79 return $this->getRateLimitReason($score);
82 final public function didDisconnect(array $request_state) {
83 $score = $this->getDisconnectScore($request_state);
85 $this->addScore($score);
91 * Get the number of seconds for each rate bucket.
93 * For example, a value of 60 will create one-minute buckets.
95 * @return int Number of seconds per bucket.
97 abstract protected function getBucketDuration();
101 * Get the total number of rate limit buckets to retain.
103 * @return int Total number of rate limit buckets to retain.
105 abstract protected function getBucketCount();
109 * Get the score to add when a client connects.
111 * @return double Connection score.
113 abstract protected function getConnectScore();
117 * Get the number of penalty points to add when a client hits a rate limit.
119 * @return double Penalty score.
121 abstract protected function getPenaltyScore();
125 * Get the score to add when a client disconnects.
127 * @return double Connection score.
129 abstract protected function getDisconnectScore(array $request_state);
133 * Get a human-readable explanation of why the client is being rejected.
135 * @return string Brief rejection message.
137 abstract protected function getRateLimitReason($score);
141 * Determine whether to reject a connection.
143 * @return bool True to reject the connection.
145 abstract protected function shouldRejectConnection($score);
149 * Get the APC key for the smallest stored bucket.
151 * @return string APC key for the smallest stored bucket.
154 private function getMinimumBucketCacheKey() {
155 $limit_key = $this->getLimitKey();
156 return "limit:min:{$limit_key}";
161 * Get the current bucket ID for storing rate limit scores.
163 * @return int The current bucket ID.
165 private function getCurrentBucketID() {
166 return (int)(time() / $this->getBucketDuration());
171 * Get the APC key for a given bucket.
173 * @param int Bucket to get the key for.
174 * @return string APC key for the bucket.
176 private function getBucketCacheKey($bucket_id) {
177 $limit_key = $this->getLimitKey();
178 return "limit:bucket:{$limit_key}:{$bucket_id}";
183 * Add points to the rate limit score for some client.
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
190 private function addScore($score) {
191 $is_apcu = (bool)function_exists('apcu_fetch');
193 $current = $this->getCurrentBucketID();
194 $bucket_key = $this->getBucketCacheKey($current);
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.
202 $bucket = apcu_fetch($bucket_key);
204 $bucket = apc_fetch($bucket_key);
207 if (!is_array($bucket)) {
211 $client_key = $this->getClientKey();
212 if (empty($bucket[$client_key])) {
213 $bucket[$client_key] = 0;
216 $bucket[$client_key] +
= $score;
219 @apcu_store
($bucket_key, $bucket);
221 @apc_store
($bucket_key, $bucket);
229 * Get the current rate limit score for a given client.
231 * @return float The client's current score.
234 private function getScore() {
235 $is_apcu = (bool)function_exists('apcu_fetch');
237 // Identify the oldest bucket stored in APC.
238 $min_key = $this->getMinimumBucketCacheKey();
240 $min = apcu_fetch($min_key);
242 $min = apc_fetch($min_key);
245 // If we don't have any buckets stored yet, store the current bucket as
246 // the oldest bucket.
247 $cur = $this->getCurrentBucketID();
250 @apcu_store
($min_key, $cur);
252 @apc_store
($min_key, $cur);
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);
264 apcu_delete($bucket_key);
265 @apcu_store
($min_key, $cursor +
1);
267 apc_delete($bucket_key);
268 @apc_store
($min_key, $cursor +
1);
272 $client_key = $this->getClientKey();
274 // Now, sum up the client's scores in all of the active buckets.
276 for (; $cursor <= $cur; $cursor++
) {
277 $bucket_key = $this->getBucketCacheKey($cursor);
279 $bucket = apcu_fetch($bucket_key);
281 $bucket = apc_fetch($bucket_key);
283 if (isset($bucket[$client_key])) {
284 $score +
= $bucket[$client_key];