mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-13 02:12:41 +01:00
ebff07d019
Summary: Ref T4571. When a database goes down briefly, we fall back to replicas. However, this fallback is slow (not good for users) and keeps sending a lot of traffic to the master (might be bad if the root cause is load-related). Keep track of recent connections and fully degrade into "severed" mode if we see a sequence of failures over a reasonable period of time. In this mode, we send much less traffic to the master (faster for users; less load for the database). We do send a little bit of traffic still, and if the master recovers we'll recover back into normal mode seeing several connections in a row succeed. This is similar to what most load balancers do when pulling web servers in and out of pools. For now, the specific numbers are: - We do at most one health check every 3 seconds. - If 5 checks in a row fail or succeed, we sever or un-sever the database (so it takes about 15 seconds to switch modes). - If the database is currently marked unhealthy, we reduce timeouts and retries when connecting to it. Test Plan: - Configured a bad `master`. - Browsed around for a bit, initially saw "unrechable master" errors. - After about 15 seconds, saw "major interruption" errors instead. - Fixed the config for `master`. - Browsed around for a while longer. - After about 15 seconds, things recovered. - Used "Cluster Databases" console to keep an eye on health checks: it now shows how many recent health checks were good: {F1213397} Reviewers: chad Reviewed By: chad Maniphest Tasks: T4571 Differential Revision: https://secure.phabricator.com/D15677
361 lines
9.1 KiB
PHP
361 lines
9.1 KiB
PHP
<?php
|
|
|
|
/**
|
|
*
|
|
* @task request Request Cache
|
|
* @task immutable Immutable Cache
|
|
* @task setup Setup Cache
|
|
* @task compress Compression
|
|
*/
|
|
final class PhabricatorCaches extends Phobject {
|
|
|
|
private static $requestCache;
|
|
|
|
public static function getNamespace() {
|
|
return PhabricatorEnv::getEnvConfig('phabricator.cache-namespace');
|
|
}
|
|
|
|
private static function newStackFromCaches(array $caches) {
|
|
$caches = self::addNamespaceToCaches($caches);
|
|
$caches = self::addProfilerToCaches($caches);
|
|
return id(new PhutilKeyValueCacheStack())
|
|
->setCaches($caches);
|
|
}
|
|
|
|
/* -( Request Cache )------------------------------------------------------ */
|
|
|
|
|
|
/**
|
|
* Get a request cache stack.
|
|
*
|
|
* This cache stack is destroyed after each logical request. In particular,
|
|
* it is destroyed periodically by the daemons, while `static` caches are
|
|
* not.
|
|
*
|
|
* @return PhutilKeyValueCacheStack Request cache stack.
|
|
*/
|
|
public static function getRequestCache() {
|
|
if (!self::$requestCache) {
|
|
self::$requestCache = new PhutilInRequestKeyValueCache();
|
|
}
|
|
return self::$requestCache;
|
|
}
|
|
|
|
|
|
/**
|
|
* Destroy the request cache.
|
|
*
|
|
* This is called at the beginning of each logical request.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function destroyRequestCache() {
|
|
self::$requestCache = null;
|
|
}
|
|
|
|
|
|
/* -( Immutable Cache )---------------------------------------------------- */
|
|
|
|
|
|
/**
|
|
* Gets an immutable cache stack.
|
|
*
|
|
* This stack trades mutability away for improved performance. Normally, it is
|
|
* APC + DB.
|
|
*
|
|
* In the general case with multiple web frontends, this stack can not be
|
|
* cleared, so it is only appropriate for use if the value of a given key is
|
|
* permanent and immutable.
|
|
*
|
|
* @return PhutilKeyValueCacheStack Best immutable stack available.
|
|
* @task immutable
|
|
*/
|
|
public static function getImmutableCache() {
|
|
static $cache;
|
|
if (!$cache) {
|
|
$caches = self::buildImmutableCaches();
|
|
$cache = self::newStackFromCaches($caches);
|
|
}
|
|
return $cache;
|
|
}
|
|
|
|
|
|
/**
|
|
* Build the immutable cache stack.
|
|
*
|
|
* @return list<PhutilKeyValueCache> List of caches.
|
|
* @task immutable
|
|
*/
|
|
private static function buildImmutableCaches() {
|
|
$caches = array();
|
|
|
|
$apc = new PhutilAPCKeyValueCache();
|
|
if ($apc->isAvailable()) {
|
|
$caches[] = $apc;
|
|
}
|
|
|
|
$caches[] = new PhabricatorKeyValueDatabaseCache();
|
|
|
|
return $caches;
|
|
}
|
|
|
|
|
|
/* -( Repository Graph Cache )--------------------------------------------- */
|
|
|
|
|
|
public static function getRepositoryGraphL1Cache() {
|
|
static $cache;
|
|
if (!$cache) {
|
|
$caches = self::buildRepositoryGraphL1Caches();
|
|
$cache = self::newStackFromCaches($caches);
|
|
}
|
|
return $cache;
|
|
}
|
|
|
|
private static function buildRepositoryGraphL1Caches() {
|
|
$caches = array();
|
|
|
|
$request = new PhutilInRequestKeyValueCache();
|
|
$request->setLimit(32);
|
|
$caches[] = $request;
|
|
|
|
$apc = new PhutilAPCKeyValueCache();
|
|
if ($apc->isAvailable()) {
|
|
$caches[] = $apc;
|
|
}
|
|
|
|
return $caches;
|
|
}
|
|
|
|
public static function getRepositoryGraphL2Cache() {
|
|
static $cache;
|
|
if (!$cache) {
|
|
$caches = self::buildRepositoryGraphL2Caches();
|
|
$cache = self::newStackFromCaches($caches);
|
|
}
|
|
return $cache;
|
|
}
|
|
|
|
private static function buildRepositoryGraphL2Caches() {
|
|
$caches = array();
|
|
$caches[] = new PhabricatorKeyValueDatabaseCache();
|
|
return $caches;
|
|
}
|
|
|
|
|
|
/* -( Setup Cache )-------------------------------------------------------- */
|
|
|
|
|
|
/**
|
|
* Highly specialized cache for performing setup checks. We use this cache
|
|
* to determine if we need to run expensive setup checks when the page
|
|
* loads. Without it, we would need to run these checks every time.
|
|
*
|
|
* Normally, this cache is just APC. In the absence of APC, this cache
|
|
* degrades into a slow, quirky on-disk cache.
|
|
*
|
|
* NOTE: Do not use this cache for anything else! It is not a general-purpose
|
|
* cache!
|
|
*
|
|
* @return PhutilKeyValueCacheStack Most qualified available cache stack.
|
|
* @task setup
|
|
*/
|
|
public static function getSetupCache() {
|
|
static $cache;
|
|
if (!$cache) {
|
|
$caches = self::buildSetupCaches();
|
|
$cache = self::newStackFromCaches($caches);
|
|
}
|
|
return $cache;
|
|
}
|
|
|
|
|
|
/**
|
|
* @task setup
|
|
*/
|
|
private static function buildSetupCaches() {
|
|
// If this is the CLI, just build a setup cache.
|
|
if (php_sapi_name() == 'cli') {
|
|
return array();
|
|
}
|
|
|
|
// In most cases, we should have APC. This is an ideal cache for our
|
|
// purposes -- it's fast and empties on server restart.
|
|
$apc = new PhutilAPCKeyValueCache();
|
|
if ($apc->isAvailable()) {
|
|
return array($apc);
|
|
}
|
|
|
|
// If we don't have APC, build a poor approximation on disk. This is still
|
|
// much better than nothing; some setup steps are quite slow.
|
|
$disk_path = self::getSetupCacheDiskCachePath();
|
|
if ($disk_path) {
|
|
$disk = new PhutilOnDiskKeyValueCache();
|
|
$disk->setCacheFile($disk_path);
|
|
$disk->setWait(0.1);
|
|
if ($disk->isAvailable()) {
|
|
return array($disk);
|
|
}
|
|
}
|
|
|
|
return array();
|
|
}
|
|
|
|
|
|
/**
|
|
* @task setup
|
|
*/
|
|
private static function getSetupCacheDiskCachePath() {
|
|
// The difficulty here is in choosing a path which will change on server
|
|
// restart (we MUST have this property), but as rarely as possible
|
|
// otherwise (we desire this property to give the cache the best hit rate
|
|
// we can).
|
|
|
|
// Unfortunately, we don't have a very good strategy for minimizing the
|
|
// churn rate of the cache. We previously tried to use the parent process
|
|
// PID in some cases, but this was not reliable. See T9599 for one case of
|
|
// this.
|
|
|
|
$pid_basis = getmypid();
|
|
|
|
// If possible, we also want to know when the process launched, so we can
|
|
// drop the cache if a process restarts but gets the same PID an earlier
|
|
// process had. "/proc" is not available everywhere (e.g., not on OSX), but
|
|
// check if we have it.
|
|
$epoch_basis = null;
|
|
$stat = @stat("/proc/{$pid_basis}");
|
|
if ($stat !== false) {
|
|
$epoch_basis = $stat['ctime'];
|
|
}
|
|
|
|
$tmp_dir = sys_get_temp_dir();
|
|
|
|
$tmp_path = $tmp_dir.DIRECTORY_SEPARATOR.'phabricator-setup';
|
|
if (!file_exists($tmp_path)) {
|
|
@mkdir($tmp_path);
|
|
}
|
|
|
|
$is_ok = self::testTemporaryDirectory($tmp_path);
|
|
if (!$is_ok) {
|
|
$tmp_path = $tmp_dir;
|
|
$is_ok = self::testTemporaryDirectory($tmp_path);
|
|
if (!$is_ok) {
|
|
// We can't find anywhere to write the cache, so just bail.
|
|
return null;
|
|
}
|
|
}
|
|
|
|
$tmp_name = 'setup-'.$pid_basis;
|
|
if ($epoch_basis) {
|
|
$tmp_name .= '.'.$epoch_basis;
|
|
}
|
|
$tmp_name .= '.cache';
|
|
|
|
return $tmp_path.DIRECTORY_SEPARATOR.$tmp_name;
|
|
}
|
|
|
|
|
|
/**
|
|
* @task setup
|
|
*/
|
|
private static function testTemporaryDirectory($dir) {
|
|
if (!@file_exists($dir)) {
|
|
return false;
|
|
}
|
|
if (!@is_dir($dir)) {
|
|
return false;
|
|
}
|
|
if (!@is_writable($dir)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static function addProfilerToCaches(array $caches) {
|
|
foreach ($caches as $key => $cache) {
|
|
$pcache = new PhutilKeyValueCacheProfiler($cache);
|
|
$pcache->setProfiler(PhutilServiceProfiler::getInstance());
|
|
$caches[$key] = $pcache;
|
|
}
|
|
return $caches;
|
|
}
|
|
|
|
private static function addNamespaceToCaches(array $caches) {
|
|
$namespace = self::getNamespace();
|
|
if (!$namespace) {
|
|
return $caches;
|
|
}
|
|
|
|
foreach ($caches as $key => $cache) {
|
|
$ncache = new PhutilKeyValueCacheNamespace($cache);
|
|
$ncache->setNamespace($namespace);
|
|
$caches[$key] = $ncache;
|
|
}
|
|
|
|
return $caches;
|
|
}
|
|
|
|
|
|
/**
|
|
* Deflate a value, if deflation is available and has an impact.
|
|
*
|
|
* If the value is larger than 1KB, we have `gzdeflate()`, we successfully
|
|
* can deflate it, and it benefits from deflation, we deflate it. Otherwise
|
|
* we leave it as-is.
|
|
*
|
|
* Data can later be inflated with @{method:inflateData}.
|
|
*
|
|
* @param string String to attempt to deflate.
|
|
* @return string|null Deflated string, or null if it was not deflated.
|
|
* @task compress
|
|
*/
|
|
public static function maybeDeflateData($value) {
|
|
$len = strlen($value);
|
|
if ($len <= 1024) {
|
|
return null;
|
|
}
|
|
|
|
if (!function_exists('gzdeflate')) {
|
|
return null;
|
|
}
|
|
|
|
$deflated = gzdeflate($value);
|
|
if ($deflated === false) {
|
|
return null;
|
|
}
|
|
|
|
$deflated_len = strlen($deflated);
|
|
if ($deflated_len >= ($len / 2)) {
|
|
return null;
|
|
}
|
|
|
|
return $deflated;
|
|
}
|
|
|
|
|
|
/**
|
|
* Inflate data previously deflated by @{method:maybeDeflateData}.
|
|
*
|
|
* @param string Deflated data, from @{method:maybeDeflateData}.
|
|
* @return string Original, uncompressed data.
|
|
* @task compress
|
|
*/
|
|
public static function inflateData($value) {
|
|
if (!function_exists('gzinflate')) {
|
|
throw new Exception(
|
|
pht(
|
|
'%s is not available; unable to read deflated data!',
|
|
'gzinflate()'));
|
|
}
|
|
|
|
$value = gzinflate($value);
|
|
if ($value === false) {
|
|
throw new Exception(pht('Failed to inflate data!'));
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
|
|
}
|