Path: blob/master/support/startup/PhabricatorClientLimit.php
12240 views
<?php12abstract class PhabricatorClientLimit {34private $limitKey;5private $clientKey;6private $limit;78final public function setLimitKey($limit_key) {9$this->limitKey = $limit_key;10return $this;11}1213final public function getLimitKey() {14return $this->limitKey;15}1617final public function setClientKey($client_key) {18$this->clientKey = $client_key;19return $this;20}2122final public function getClientKey() {23return $this->clientKey;24}2526final public function setLimit($limit) {27$this->limit = $limit;28return $this;29}3031final public function getLimit() {32return $this->limit;33}3435final public function didConnect() {36// NOTE: We can not use pht() here because this runs before libraries37// load.3839if (!function_exists('apc_fetch') && !function_exists('apcu_fetch')) {40throw new Exception(41'You can not configure connection rate limits unless APC/APCu are '.42'available. Rate limits rely on APC/APCu to track clients and '.43'connections.');44}4546if ($this->getClientKey() === null) {47throw new Exception(48'You must configure a client key when defining a rate limit.');49}5051if ($this->getLimitKey() === null) {52throw new Exception(53'You must configure a limit key when defining a rate limit.');54}5556if ($this->getLimit() === null) {57throw new Exception(58'You must configure a limit when defining a rate limit.');59}6061$points = $this->getConnectScore();62if ($points) {63$this->addScore($points);64}6566$score = $this->getScore();67if (!$this->shouldRejectConnection($score)) {68// Client has not hit the limit, so continue processing the request.69return null;70}7172$penalty = $this->getPenaltyScore();73if ($penalty) {74$this->addScore($penalty);75$score += $penalty;76}7778return $this->getRateLimitReason($score);79}8081final public function didDisconnect(array $request_state) {82$score = $this->getDisconnectScore($request_state);83if ($score) {84$this->addScore($score);85}86}878889/**90* Get the number of seconds for each rate bucket.91*92* For example, a value of 60 will create one-minute buckets.93*94* @return int Number of seconds per bucket.95*/96abstract protected function getBucketDuration();979899/**100* Get the total number of rate limit buckets to retain.101*102* @return int Total number of rate limit buckets to retain.103*/104abstract protected function getBucketCount();105106107/**108* Get the score to add when a client connects.109*110* @return double Connection score.111*/112abstract protected function getConnectScore();113114115/**116* Get the number of penalty points to add when a client hits a rate limit.117*118* @return double Penalty score.119*/120abstract protected function getPenaltyScore();121122123/**124* Get the score to add when a client disconnects.125*126* @return double Connection score.127*/128abstract protected function getDisconnectScore(array $request_state);129130131/**132* Get a human-readable explanation of why the client is being rejected.133*134* @return string Brief rejection message.135*/136abstract protected function getRateLimitReason($score);137138139/**140* Determine whether to reject a connection.141*142* @return bool True to reject the connection.143*/144abstract protected function shouldRejectConnection($score);145146147/**148* Get the APC key for the smallest stored bucket.149*150* @return string APC key for the smallest stored bucket.151* @task ratelimit152*/153private function getMinimumBucketCacheKey() {154$limit_key = $this->getLimitKey();155return "limit:min:{$limit_key}";156}157158159/**160* Get the current bucket ID for storing rate limit scores.161*162* @return int The current bucket ID.163*/164private function getCurrentBucketID() {165return (int)(time() / $this->getBucketDuration());166}167168169/**170* Get the APC key for a given bucket.171*172* @param int Bucket to get the key for.173* @return string APC key for the bucket.174*/175private function getBucketCacheKey($bucket_id) {176$limit_key = $this->getLimitKey();177return "limit:bucket:{$limit_key}:{$bucket_id}";178}179180181/**182* Add points to the rate limit score for some client.183*184* @param string Some key which identifies the client making the request.185* @param float The cost for this request; more points pushes them toward186* the limit faster.187* @return this188*/189private function addScore($score) {190$is_apcu = (bool)function_exists('apcu_fetch');191192$current = $this->getCurrentBucketID();193$bucket_key = $this->getBucketCacheKey($current);194195// There's a bit of a race here, if a second process reads the bucket196// before this one writes it, but it's fine if we occasionally fail to197// record a client's score. If they're making requests fast enough to hit198// rate limiting, we'll get them soon enough.199200if ($is_apcu) {201$bucket = apcu_fetch($bucket_key);202} else {203$bucket = apc_fetch($bucket_key);204}205206if (!is_array($bucket)) {207$bucket = array();208}209210$client_key = $this->getClientKey();211if (empty($bucket[$client_key])) {212$bucket[$client_key] = 0;213}214215$bucket[$client_key] += $score;216217if ($is_apcu) {218@apcu_store($bucket_key, $bucket);219} else {220@apc_store($bucket_key, $bucket);221}222223return $this;224}225226227/**228* Get the current rate limit score for a given client.229*230* @return float The client's current score.231* @task ratelimit232*/233private function getScore() {234$is_apcu = (bool)function_exists('apcu_fetch');235236// Identify the oldest bucket stored in APC.237$min_key = $this->getMinimumBucketCacheKey();238if ($is_apcu) {239$min = apcu_fetch($min_key);240} else {241$min = apc_fetch($min_key);242}243244// If we don't have any buckets stored yet, store the current bucket as245// the oldest bucket.246$cur = $this->getCurrentBucketID();247if (!$min) {248if ($is_apcu) {249@apcu_store($min_key, $cur);250} else {251@apc_store($min_key, $cur);252}253$min = $cur;254}255256// Destroy any buckets that are older than the minimum bucket we're keeping257// track of. Under load this normally shouldn't do anything, but will clean258// up an old bucket once per minute.259$count = $this->getBucketCount();260for ($cursor = $min; $cursor < ($cur - $count); $cursor++) {261$bucket_key = $this->getBucketCacheKey($cursor);262if ($is_apcu) {263apcu_delete($bucket_key);264@apcu_store($min_key, $cursor + 1);265} else {266apc_delete($bucket_key);267@apc_store($min_key, $cursor + 1);268}269}270271$client_key = $this->getClientKey();272273// Now, sum up the client's scores in all of the active buckets.274$score = 0;275for (; $cursor <= $cur; $cursor++) {276$bucket_key = $this->getBucketCacheKey($cursor);277if ($is_apcu) {278$bucket = apcu_fetch($bucket_key);279} else {280$bucket = apc_fetch($bucket_key);281}282if (isset($bucket[$client_key])) {283$score += $bucket[$client_key];284}285}286287return $score;288}289290}291292293