1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-14 10:52:41 +01:00
phorge-phorge/support/startup/PhabricatorClientLimit.php
epriestley 5eaa0f24e7 Use "@" to silence "GC list" warnings from "apc_store()" and "apcu_store()"
Summary:
Fixes T13525. Since D21044, the intermittent GC list warnings are treated more severely and can become user-visible errors.

Silence them, since this seems to be the only realistic response in most versions of APC/APCu.

Test Plan: Will deploy.

Maniphest Tasks: T13525

Differential Revision: https://secure.phabricator.com/D21179
2020-04-28 04:13:37 -07:00

291 lines
7.1 KiB
PHP

<?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;
}
}