Provide missing default attachment list for Files transactions
[phabricator.git] / support / startup / PhabricatorClientLimit.php
blobc43e4b42c08fec63358b8b6c35af847ab08ff8c1
1 <?php
3 abstract class PhabricatorClientLimit {
5 private $limitKey;
6 private $clientKey;
7 private $limit;
9 final public function setLimitKey($limit_key) {
10 $this->limitKey = $limit_key;
11 return $this;
14 final public function getLimitKey() {
15 return $this->limitKey;
18 final public function setClientKey($client_key) {
19 $this->clientKey = $client_key;
20 return $this;
23 final public function getClientKey() {
24 return $this->clientKey;
27 final public function setLimit($limit) {
28 $this->limit = $limit;
29 return $this;
32 final public function getLimit() {
33 return $this->limit;
36 final public function didConnect() {
37 // NOTE: We can not use pht() here because this runs before libraries
38 // load.
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.');
47 if ($this->getClientKey() === null) {
48 throw new Exception(
49 'You must configure a client key when defining a rate limit.');
52 if ($this->getLimitKey() === null) {
53 throw new Exception(
54 'You must configure a limit key when defining a rate limit.');
57 if ($this->getLimit() === null) {
58 throw new Exception(
59 'You must configure a limit when defining a rate limit.');
62 $points = $this->getConnectScore();
63 if ($points) {
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.
70 return null;
73 $penalty = $this->getPenaltyScore();
74 if ($penalty) {
75 $this->addScore($penalty);
76 $score += $penalty;
79 return $this->getRateLimitReason($score);
82 final public function didDisconnect(array $request_state) {
83 $score = $this->getDisconnectScore($request_state);
84 if ($score) {
85 $this->addScore($score);
90 /**
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.
152 * @task ratelimit
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
187 * the limit faster.
188 * @return this
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.
201 if ($is_apcu) {
202 $bucket = apcu_fetch($bucket_key);
203 } else {
204 $bucket = apc_fetch($bucket_key);
207 if (!is_array($bucket)) {
208 $bucket = array();
211 $client_key = $this->getClientKey();
212 if (empty($bucket[$client_key])) {
213 $bucket[$client_key] = 0;
216 $bucket[$client_key] += $score;
218 if ($is_apcu) {
219 @apcu_store($bucket_key, $bucket);
220 } else {
221 @apc_store($bucket_key, $bucket);
224 return $this;
229 * Get the current rate limit score for a given client.
231 * @return float The client's current score.
232 * @task ratelimit
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();
239 if ($is_apcu) {
240 $min = apcu_fetch($min_key);
241 } else {
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();
248 if (!$min) {
249 if ($is_apcu) {
250 @apcu_store($min_key, $cur);
251 } else {
252 @apc_store($min_key, $cur);
254 $min = $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);
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);
272 $client_key = $this->getClientKey();
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);
283 if (isset($bucket[$client_key])) {
284 $score += $bucket[$client_key];
288 return $score;